命令注入
-
攻击原理
命令注入是指开发者使用未经校验的不可信输入作为系统命令的参数或命令的一部分,使得攻击者可以利用这个漏洞构造相关系统命令语句,执行一些非法越权操作。
注:听起来是不是与SQL
注入十分类似,其实只要是注入,攻击方式都是类似的,因为注入的本质就是:程序没有有效区分“代码”和“数据”。 -
攻击影响
对于命令注入漏洞,命令将会以与Go
应用程序相同的特权级别执行,它向攻击者提供了类似系统shell
的功能。 -
防范措施
在Go
中,os/exec
经常被用来调用一个新的进程,如果被执行的命令拼接了外部不可信输入,则可能会产生命令和参数注入。执行命令的时候,需要注意以下几点:
- 命令执行的字符串不要去拼接输入的参数,如果必须拼接时,则需要对输入参数进行白名单过滤;
- 对传入的参数要做类型校验。例如整数数据,需要对数据进行整数强制转换;
- 保证格式化字符串的正确性。例如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
,添加如下代码(老惯例,仍然是声明了两个对比的控制器,并分别重写了Get
和Post
函数):
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
请求时,一个表单会发送至CommandController
的Post
函数响应并处理,而另一个表单会发送至SafeCommandController
的Post
函数响应并处理。
放两个文件
在页面中输入要删除的文件名,而控制器里的代码则默认删除的文件目录在\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
中将对应文件删除。
然而在CommandController
的Post
函数中,由于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
,添加如下代码(老惯例,仍然是声明了两个对比的控制器,并分别重写了Get
和Post
函数):
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
请求时,一个表单会发送至XMLController
的Post
函数响应并处理,而另一个表单会发送至SafeXMLController
的Post
函数响应并处理。
进行实验
在浏览器中输入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
注入的情况,这里用户输入的"<"
和">"
都被分别转义成了"<"
和">"
。
后台显示如下:
可以从图片上半部分的日志输出中看到,这一次只产生了一个User
,只不过这个User
密码比较长罢了,读取出来的密码是原输入值,而保存进XML
文件里的密码是转义过后的值。
原因分析
表单的本意设计是输入ID
,名字,密码,能在\static\upload
生成一个XML
文件保存输入的用户数据。
然而在XMLController
的Post
函数中,直接使用用户输入的参数进行字符串拼接:
// 现在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})
更多对代码的解释都已经写在代码注释里了。