GO语言Beego框架之WEB安全小系统(4)命令注入与XML注入

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/A657997301/article/details/82624002

命令注入

  • 攻击原理
    命令注入是指开发者使用未经校验的不可信输入作为系统命令的参数或命令的一部分,使得攻击者可以利用这个漏洞构造相关系统命令语句,执行一些非法越权操作。
    注:听起来是不是与SQL注入十分类似,其实只要是注入,攻击方式都是类似的,因为注入的本质就是:程序没有有效区分“代码”和“数据”

  • 攻击影响
    对于命令注入漏洞,命令将会以与Go应用程序相同的特权级别执行,它向攻击者提供了类似系统shell的功能。

  • 防范措施
    Go中,os/exec经常被用来调用一个新的进程,如果被执行的命令拼接了外部不可信输入,则可能会产生命令和参数注入。执行命令的时候,需要注意以下几点:
    1) 命令执行的字符串不要去拼接输入的参数,如果必须拼接时,则需要对输入参数进行白名单过滤;
    2) 对传入的参数要做类型校验。例如整数数据,需要对数据进行整数强制转换;
    3) 保证格式化字符串的正确性。例如int类型参数的拼接,对于参数要用%d,不能用%s

添加代码

views部分

views 文件夹里新建一个File,命名为CommandController.tpl ,添加如下代码(即在body标签里添加两个表单,各放一个input 输入要删除的文件名):

<div class="postform">
            <p> 命令注入 </p>
            <form id="user" action="http://127.0.0.1:8080/problems/CommandInjection" method="post">
                输入要删除的文件:<input name="filename" type="text"> <br>
                <input type="submit" value="提交">
            </form>
            <br><br><br><br>
            <p> 命令注入防范 </p>
            <form id="user" action="http://127.0.0.1:8080/problems/SafeCommandInjection" method="post">
                输入要删除的文件:<input name="filename" type="text"> <br>
                <input type="submit" value="提交">
            </form>
        </div>

controllers部分

controllers 文件夹里新建一个go文件,命名为CommandInjection.go ,添加如下代码(老惯例,仍然是声明了两个对比的控制器,并分别重写了GetPost函数):

package controllers

import (
    "log"
    "fmt"
    "github.com/astaxie/beego"
    "os/exec"
    "os"
)

// 命令注入问题
type CommandController struct {
    beego.Controller
}

// 对应上传的文件名字
type filename struct {
    StrFile string `form:"filename"`
}

func (c *CommandController) Get() {
    c.TplName = "CommandController.tpl"
}

// 获取传过来的命令参数(要删除的文件名字)
func (c *CommandController) Post() {
    // 新建一个user
    u := &filename{}
    // 将c,即CommandController,里存储的数据转化到filename格式的u变量,注意必须传入地址
    if err := c.ParseForm(u); err != nil {
        log.Fatal("ParseForm err ", err)
    }
    // exec.Command("cmd", "/C", "cd.> static\\upload\\a.txt && cd.> static\\upload\\b.txt").Run()
    /** 【错误】允许调用OS命令解析器,也没有对入参做合法性校验**/
    // 注入:a.txt && del static\upload\b.txt
    // a.txt && cd.> static\upload\b.txt
    cmd := exec.Command("cmd", "/c", "del static\\upload\\"+u.StrFile)
    fmt.Println("del static\\upload\\"+u.StrFile)
    if err := cmd.Run(); err != nil {
        fmt.Println("delete file error: ", err)
    }
    c.TplName = "CommandController.tpl"
}

// 命令注入防范
type SafeCommandController struct {
    beego.Controller
}

func (c *SafeCommandController) Get() {
    c.TplName = "CommandController.tpl"
}

func (c *SafeCommandController) Post() {
    // 新建一个user
    u := &filename{}
    // 将c,即CommandController,里存储的数据转化到filename格式的u变量,注意必须传入地址
    if err := c.ParseForm(u); err != nil {
        log.Fatal("ParseForm err ", err)
    }
    param := "static\\upload\\"+u.StrFile
    err := os.Remove(param) /**【修改】使用Go的os.Remove()删除文件**/
    if err != nil {
        fmt.Println("delete file error.", err)
    }
    c.TplName = "CommandController.tpl"
}

routers部分

routers/router.go 文件添加如下代码(即为上述两个控制器注册路由):

// 命令注入问题
    beego.Router("/problems/CommandInjection", &controllers.CommandController{})
    beego.Router("/problems/SafeCommandInjection", &controllers.SafeCommandController{})

这样,无论url是访问/problems/CommandInjection 还是/problems/SafeCommandInjection,两种Get请求都能正确渲染CommandController.tpl这个页面,然后当从表单发送Post请求时,一个表单会发送至CommandControllerPost函数响应并处理,而另一个表单会发送至SafeCommandControllerPost函数响应并处理。

放两个文件

在页面中输入要删除的文件名,而控制器里的代码则默认删除的文件目录在\static\upload ,因此,我们还需要先在该目录下新建两个文件作实验用。
这里写图片描述

进行实验

在浏览器中输入http://127.0.0.1:8080/problems/SQLInjection
这里写图片描述

正常情况

在“命令注入”的表单里填写“b.txt”并提交:
这里写图片描述

后台显示如下:
这里写图片描述

cmd命令中del的意思即为删除,这里b.txt文件被成功删除。

命令注入

在“命令注入”的表单里填写“a.txt && cd.> static\upload\b.txt”并提交:
这里写图片描述

后台显示如下:
这里写图片描述

a.txt被成功删除,而upload却被新建了一个b.txt

命令注入防范

在“命令注入防范”的表单里填写“a.txt && cd.> static\upload\b.txt”并提交:
这里写图片描述

后台显示如下:
这里写图片描述

后台输出文件名或者目录名不正确,因此删除失败。

原因分析

表单的本意设计是输入一个文件名,能在\static\upload 中将对应文件删除。

然而在CommandControllerPost函数中,由于exec.Command()函数本身不会对参数进行校验,程序员也没有对参数做合法性校验,再加上命令语句的&&符号可以使多个命令连续执行。

所以当输入:

a.txt && cd.> static\upload\b.txt

执行的命令语句就变成了:

del static\upload\a.txt && cd.> static\upload\b.txt

于是便产生了命令注入。

/** 【错误】允许调用OS命令解析器,也没有对入参做合法性校验**/
cmd := exec.Command("cmd", "/c", "del static\\upload\\"+u.StrFile)

推荐防范措施:禁止调用OS命令解析器,使用其它标准API替代,它从根本上消除了发生命令注入和参数注入的可能。针对本例而言,它是用来删除指定文件的,所以可使用Go语言的os.Remove()来替代:

param := "static\\upload\\"+u.StrFile
err := os.Remove(param) /**【修改】使用Go的os.Remove()删除文件**/

调用os.Remove() ,上来程序便知道开发者的本意是删除,因而不可能被攻击者传入的非法参数而误导。在实际运行该函数时再传入文件名参数,这种做法跟SQL注入的预编译参数化十分类似。

XML注入

  • 攻击原理
    使用未经校验数据来构造XML会导致XML注入漏洞。如果用户被允许输入结构化的XML片段,则他可以在XML的数据域中注入XML标签来改写目标XML文档的结构和内容,XML解析器会对注入的标签进行识别和解释,引起注入问题。

添加代码

views部分

views 文件夹里新建一个File,命名为XMLController.tpl ,添加如下代码(即在body标签里添加两个表单,表单可以提交ID/名字/密码):

        <div class="postform">
            <p> XML注入 </p>
            <form id="user" action="http://127.0.0.1:8080/problems/XMLInjection" method="post">
                  ID:<input name="id" type="text"> <br>
                名字:<input name="name" type="text"> <br>
                密码:<input name="password" type="text"> <br>
                <input type="submit" value="提交">
            </form>
            <br><br><br><br>
            <p> XML注入防范 </p>
            <form id="user" action="http://127.0.0.1:8080/problems/SafeXMLInjection" method="post">
                  ID:<input name="id" type="text"> <br>
                名字:<input name="name" type="text"> <br>
                密码:<input name="password" type="text"> <br>
                <input type="submit" value="提交">
            </form>
        </div>

controllers部分

controllers 文件夹里新建一个go文件,命名为XMLController.go ,添加如下代码(老惯例,仍然是声明了两个对比的控制器,并分别重写了GetPost函数):

package controllers

import (
    "fmt"
    "log"
    "github.com/astaxie/beego"
    "encoding/xml"
    "io/ioutil"
    "os"
    "strconv"
)

// XML注入问题
type XMLController struct {
    beego.Controller
}

func (c *XMLController) Get() {
    c.TplName = "XMLController.tpl"
}

type headers struct {
    XMLName         xml.Name    `xml:"Users"`
    Header          []header    `xml:"User"`
}

type header struct {
    Id          int     `xml:"id"`
    Name        string  `xml:"name"`
    Password    string  `xml:"password"`
}

// 获取传过来的数据参数,保存为XML文件
func (c *XMLController) Post() {
    // 新建一个User
    u := &User{}
    // 将c,XMLController,里存储的数据转化到User格式的u变量,注意必须传入地址
    if err := c.ParseForm(u); err != nil {
        log.Fatal("ParseForm err ", err)
    }
    // 现在u是个有数据的User了,取出来存到c里去
    userdata := "<Users>\n\t<User>\n\t\t<id>"+strconv.Itoa(u.Id)+"</id>\n\t\t<name>"+u.Username+"</name>\n\t\t<password>"+u.Password+"</password>\n\t</User>\n</Users>"
    // 加入XML头
    headerBytes := []byte(xml.Header)
    // 拼接XML头和体
    xmlOutPutData := append(headerBytes, userdata...)
    // 将[]byte的xmlOutPutData转换成string输出
    fmt.Println(string(xmlOutPutData))
    // 保存成xml文件
    ioutil.WriteFile("static/upload/aaa.xml", xmlOutPutData, os.ModeAppend)
    c.TplName = "XMLController.tpl"
    // 读取XML文件数据并输出
    content, err := ioutil.ReadFile("static/upload/aaa.xml")
    if err != nil {
        log.Fatal(err)
    }
    var result headers
    // 将XML解析成[]byte
    err = xml.Unmarshal(content, &result)
    if err != nil {
        log.Fatal(err)
    }
    // 输出整个读取结果
    log.Println(result)
    // 输出结果的头部分,即有数据的部分
    log.Println(result.Header)
    // 将有数据部分里的User分别输出
    for _, o := range result.Header {
        log.Println(o.Id)
        log.Println(o.Name)
        log.Println(o.Password)
    }
}

// XML注入防范
type SafeXMLController struct {
    beego.Controller
}

func (c *SafeXMLController) Get() {
    c.TplName = "XMLController.tpl"
}

// 87654321</password></User><User><id>250</id><name>Stella Chan</name><password>250250
func (c *SafeXMLController) Post() {
    // 新建一个User
    u := &User{}
    // 将c,XMLController,里存储的数据转化到User格式的u变量,注意必须传入地址
    if err := c.ParseForm(u); err != nil {
        log.Fatal("ParseForm err ", err)
    }
    // 新建一个headers作为XML的总部分
    v := &headers{}
    // 添加头部分
    v.Header = append(v.Header, header{u.Id, u.Username, u.Password})
    // 对XML添加标签前缀的空格
    output, err := xml.MarshalIndent(v, "  ", "    ")
    if err != nil {
        fmt.Printf("error: %v\n", err)
    }
    // 加入XML头
    headerBytes := []byte(xml.Header)
    // 拼接XML头和体
    xmlOutPutData := append(headerBytes, output...)
    // 将[]byte转换成string输出
    fmt.Println(string(xmlOutPutData))
    // 保存成xml文件
    ioutil.WriteFile("static/upload/aaa.xml", xmlOutPutData, os.ModeAppend)
    c.TplName = "XMLController.tpl"
    // 读取XML文件数据并输出
    content, err := ioutil.ReadFile("static/upload/aaa.xml")
    if err != nil {
        log.Fatal(err)
    }
    var result headers
    // 将XML解析成[]byte
    err = xml.Unmarshal(content, &result)
    if err != nil {
        log.Fatal(err)
    }
    // 输出整个读取结果
    log.Println(result)
    // 输出结果的头部分,即有数据的部分
    log.Println(result.Header)
    // 将有数据部分里的User分别输出
    for _, o := range result.Header {
        log.Println(o.Id)
        log.Println(o.Name)
        log.Println(o.Password)
    }
}

routers部分

routers/router.go 文件添加如下代码(即为上述两个控制器注册路由):

// XML注入问题
beego.Router("/problems/XMLInjection", &controllers.XMLController{})
beego.Router("/problems/SafeXMLInjection", &controllers.SafeXMLController{})

这样,无论url是访问/problems/XMLInjection 还是/problems/SafeXMLInjection,两种Get请求都能正确渲染XMLController.tpl这个页面,然后当从表单发送Post请求时,一个表单会发送至XMLControllerPost函数响应并处理,而另一个表单会发送至SafeXMLControllerPost函数响应并处理。

进行实验

在浏览器中输入http://127.0.0.1:8080/problems/XMLInjection
这里写图片描述

正常情况

在“XML注入”的表单里填写如下信息并提交:
这里写图片描述

static/upload文件夹里成功生成了aaa.xml
这里写图片描述

后台显示如下:
这里写图片描述

可知,成功生成了XML结构信息,并可以解析读取XML文件

XML注入

删掉刚才的aaa.xml 文件。
在“XML注入”的表单里填写如下信息并提交:
密码栏的输入为:87654321</password></User><User><id>250</id><name>Stella Chan</name><password>250250
这里写图片描述

static/upload文件夹里成功生成了aaa.xml
这里写图片描述

后台显示如下:
这里写图片描述

一条输入,产生了两个User,并且仍然能正常保存成XML文件,该文件里的XML结构数据也仍然能正常地被程序所读取输出。

XML注入防范

删掉刚才的aaa.xml 文件。
在“XML注入防范”的表单里填写如下信息并提交:
密码栏的输入为:87654321</password></User><User><id>250</id><name>Stella Chan</name><password>250250
这里写图片描述

static/upload文件夹里成功生成了aaa.xml
这里写图片描述

可以看到,相较于刚才XML注入的情况,这里用户输入的"<"">"都被分别转义成了"&lt;""&gt;"

后台显示如下:
这里写图片描述
可以从图片上半部分的日志输出中看到,这一次只产生了一个User,只不过这个User密码比较长罢了,读取出来的密码是原输入值,而保存进XML文件里的密码是转义过后的值。

原因分析

表单的本意设计是输入ID,名字,密码,能在\static\upload 生成一个XML文件保存输入的用户数据。

然而在XMLControllerPost函数中,直接使用用户输入的参数进行字符串拼接:

// 现在u是个有数据的User了,取出来存到c里去
userdata := "<Users>\n\t<User>\n\t\t<id>"+strconv.Itoa(u.Id)+"</id>\n\t\t<name>"+u.Username+"</name>\n\t\t<password>"+u.Password+"</password>\n\t</User>\n</Users>"

加上开发者也没有对参数做合法性校验或者对特殊字符进行转义

所以当在密码栏输入:

87654321</password></User><User><id>250</id><name>Stella Chan</name><password>250250

拼接成的字符串便成了:

<Users>\n\t<User>\n\t\t<id>66</id>\n\t\t<name>Jason</name>\n\t\t<password>87654321</password></User><User><id>250</id><name>Stella Chan</name><password>250250</password>\n\t</User>\n</Users>

攻击者可以借此自定义任何标签的数据,于是便产生了XML注入,自然也就能生成多个User用户数据了。

推荐防范措施:禁止使用字符串拼接XML语句,改为使用其它自带校验或者特殊字符转义的标准API替代。比如本例可使用Go语言的encoding/xml 包里的函数来进行XML生成:

type headers struct {
    XMLName         xml.Name    `xml:"Users"`
    Header          []header    `xml:"User"`
}

type header struct {
    Id          int     `xml:"id"`
    Name        string  `xml:"name"`
    Password    string  `xml:"password"`
}
// 新建一个headers作为XML的总部分
v := &headers{}
// 添加头部分
v.Header = append(v.Header, header{u.Id, u.Username, u.Password})

更多对代码的解释都已经写在代码注释里了。

猜你喜欢

转载自blog.csdn.net/A657997301/article/details/82624002