pyconcrete icon indicating copy to clipboard operation
pyconcrete copied to clipboard

[Security🔐] Leak source code by hacking `marshal.loads` function

Open ZhaoQi99 opened this issue 1 year ago • 19 comments
trafficstars

We can hack the marshal.loads function to get the pyc file, and then use decompyle3 to decompile and get the python source code.

Environment:

  • Python: 3.8.20
  • pyconcrete: pyconcrete "0.15.1" [Python "3.8.20"]
  • decompyle3: 3.9.2
  • OS: Debian GNU/Linux 12

Files

script.py
def fun():
    print('Hello')

fun()
hack.py
import marshal
import copy

hack = copy.deepcopy(marshal.loads)
import imp
def wrapper(*args, **kwargs):
    result = hack(*args, **kwargs)
    with open('script.pyc','wb') as f:
        f.write(imp.get_magic() + b'\x00'*12 +  args[0])
    return result
marshal.loads = wrapper

from script import *
fun()

Preparation:

~$ pyconcrete-admin.py compile --source=script.py --pye
~$ rm script.py
~$ pyconcrete script.pye
~$ pip install decompyle3

Hack:

~$ python hack.py
~$ decompyle3 script.pyc
# decompyle3 version 3.9.2
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.8.20 (default, Sep 27 2024, 06:05:08)
# [GCC 12.2.0]
# Embedded file name: script.py


def fun():
    print('Hello')

# okay decompiling script.pyc

ZhaoQi99 avatar Nov 06 '24 08:11 ZhaoQi99

> decompyle3 rb2.pye 
# file rb2.pye
# path rb2.pye must point to a Python source that can be compiled, or Python bytecode (.pyc, .pyo)
 python3 hack.py    
Traceback (most recent call last):
  File "/tmp/hack.py", line 13, in <module>
    from rb2 import *
ImportError: bad magic number in 'rb2': b'\xa2\x9f\xa4R'

6b3478 avatar Dec 05 '24 08:12 6b3478

> decompyle3 rb2.pye 
# file rb2.pye
# path rb2.pye must point to a Python source that can be compiled, or Python bytecode (.pyc, .pyo)
 python3 hack.py    
Traceback (most recent call last):
  File "/tmp/hack.py", line 13, in <module>
    from rb2 import *
ImportError: bad magic number in 'rb2': b'\xa2\x9f\xa4R'

What's your Python version? -> f.write(imp.get_magic() + b'\x00'*12 + args[0])

In [3]: imp.get_magic()
Out[3]: b'U\r\r\n'

In [4]: len(imp.get_magic())
Out[4]: 4

https://github.com/Falldog/pyconcrete/blob/0cc69150f96db5ce202be61b8e810b167fb030cc/src/pyconcrete/init.py#L43-L60

ZhaoQi99 avatar Dec 05 '24 09:12 ZhaoQi99

3.12

6b3478 avatar Dec 05 '24 09:12 6b3478

pyconcrete => python3.9-bookworm

6b3478 avatar Dec 05 '24 09:12 6b3478

3.12

imp module is remove in 3.12. https://docs.python.org/3.12/whatsnew/3.12.html#whatsnew312-removed-imp pyconcrete mayn't work under 3.12. fix: issues related to python 3.12

https://github.com/Falldog/pyconcrete/blob/0cc69150f96db5ce202be61b8e810b167fb030cc/src/pyconcrete/init.py#L55-L60

ZhaoQi99 avatar Dec 05 '24 09:12 ZhaoQi99

> decompyle3 rb2.pye 
# file rb2.pye
# path rb2.pye must point to a Python source that can be compiled, or Python bytecode (.pyc, .pyo)
 python3 hack.py    
Traceback (most recent call last):
  File "/tmp/hack.py", line 13, in <module>
    from rb2 import *
ImportError: bad magic number in 'rb2': b'\xa2\x9f\xa4R'

Why? decompyle3 rb2.pye ? It should be decompyle3 rb2.pyc

ZhaoQi99 avatar Dec 05 '24 09:12 ZhaoQi99

looks like .pyc - pythoncompile, .pye - encrypted. maybe u need to install pyconcrete this way: PYCONCRETE_PASSPHRASE="$(dd if=/dev/urandom bs=1k count=1 | head -c10 | base64)" pip3.9 install pyconcrete pyconcrete-admin.py compile -s /usr/bin/rb2.py --pye --remove-py &&
pyconcrete /usr/bin/rb2.pye --help

6b3478 avatar Dec 05 '24 12:12 6b3478

LOL =)

6b3478 avatar Dec 05 '24 12:12 6b3478

looks like .pyc - pythoncompile, .pye - encrypted. maybe u need to install pyconcrete this way: PYCONCRETE_PASSPHRASE="$(dd if=/dev/urandom bs=1k count=1 | head -c10 | base64)" pip3.9 install pyconcrete pyconcrete-admin.py compile -s /usr/bin/rb2.py --pye --remove-py && pyconcrete /usr/bin/rb2.pye --help

I don't quite understand what you mean. What you say is pyconcrete's beat practice,is it related to hack? Have you successfully reproduced the logic of the hack?

ZhaoQi99 avatar Dec 05 '24 12:12 ZhaoQi99

You can get .pyc file using wrapper in hack.py. @6b3478

ZhaoQi99 avatar Dec 05 '24 12:12 ZhaoQi99

yes. your hack don't work. may be in your home lab. also i inspect your python super-encryption-with-license repo =)) i have a friend in russia. they say: в своем глазу - бревна не замечает, а в чужом соринки разглядывает ;-) have a nice day

6b3478 avatar Dec 05 '24 16:12 6b3478

I've successfully reproduced in docker with Python 3.9. And I don't understand what you're doing and saying.

  1. You should use decompyle3 rb2.pyc, but decompyle3 rb2.pye.
  2. The logic of issue is to hack marshal.loads function to generate the .pycfile of script.py. And then translates .pyc to Python source code using decompyle3.
  3. decompile3 currently only supports Python 3.8 and below, so you may need to change L106 to version == (3, 9) https://github.com/rocky/python-decompile3/blob/2118134478b5867ecf5ce193435ba49c7baf8b11/decompyle3/parsers/main.py#L105-L108

[!NOTE]
decompyle3 translates Python bytecode back into equivalent Python source code.

  1. Python import: https://docs.python.org/3/library/importlib.html#importlib.machinery.SourcelessFileLoader

Environment

~$ docker pull python:3.9
~$ docker run --name=py39 -d python:3.9 sleep 3600000
~$ docker exec -it py39 bash

Hack

root@cde87253aac7:/pyconcrete# pyconcrete-admin.py compile --source=script.py --pye
root@cde87253aac7:/pyconcrete# rm script.py
root@cde87253aac7:/pyconcrete# python hack.py
root@cde87253aac7:/pyconcrete# vim /usr/local/lib/python3.9/site-packages/decompyle3/bin/decompile.py

root@cde87253aac7:/pyconcrete# decompyle3 script.pyc
# decompyle3 version 3.9.2
# Python bytecode version base 3.9.0 (3425)
# Decompiled from: Python 3.9.9 (main, Dec 21 2021, 10:03:34)
# [GCC 10.2.1 20210110]
# Embedded file name: script.py


def fun():
    print("Hello")


fun()

# okay decompiling ../script.pyc

[!CAUTION]
Finally, Why don't you try it in the same environment as me? And why don't you read the issue carefully? @6b3478 Btw,I am Chinese, not Russian.

[!TIP]
pyconcrete is an experimental project, there is always a way to decrypt .pye files, but pyconcrete just make it harder.

ZhaoQi99 avatar Dec 05 '24 17:12 ZhaoQi99

Hi @ZhaoQi99 Thx for you issue! Your vulnerability works but hacker have to do access to server with *.pye files with write and execute permissions If hacker has write and execute permissions to your server this "pye problem" will be the least dangerous compared to other problems)

Also your can remove pyconcrete package and launch pye files with pyconcrete binary only without importing pyconcrete in code. In this case your vulnerability does not works because "from script import *" will fails with error.

Also stealing pye files without the server pyconcrete lib package files will not help in successful decompilation. So chmod and last os updates will help you )

@Falldog May be it will be good to add this case in README.md

dx-77 avatar Dec 14 '24 21:12 dx-77

@ZhaoQi99 Already described in https://github.com/Falldog/pyconcrete/issues/23

dx-77 avatar Dec 16 '24 18:12 dx-77

Thanks the elaboration of @dx-77

@ZhaoQi99 I think you are using the partial encrypted solution.

Partial encrypted (README Link). I think there are hundreds way to hack it. If your are senior python engineer.

Recommend the Full encrypted solution (README Link). It will not allow user to import pyconrete by customized scripts. It should be "more safe" than partial encryption.

I think we should put the Deprecated or Non safety mark on the section of partial encrypted solution in README. Make developer notice it.

Falldog avatar Dec 17 '24 14:12 Falldog

@Falldog Thanks for your replay.

Yep! You are right.It seems that what I use is the partial encrypted solution. In fact, I didn't do anything extra besides installing it by python setup.py install. And /usr/local/lib/python3.9/site-packages/pyconcrete does not contain any source code.🤔 Is this still considered partial encryption? I just found out why there is no error when executing python hack.py. And I don't import pyconcrete in hack.py. It's so amazing.

~$ git clone https://github.com/Falldog/pyconcrete.git --depth=1
~$ cd pyconcrete/
~$ python setup.py install
...
copying build/scripts-3.9/pyconcrete -> /usr/local/bin
creating /usr/local/lib/python3.9/site-packages/pyconcrete.pth

After I remove /usr/local/lib/python3.9/site-packages/pyconcrete and pyconcrete.pth. python hack.py will not work,but pyconcrete script.pye still works well.

ModuleNotFoundError: No module named 'script'

root@cde87253aac7:/usr/local/lib/python3.9/site-packages/pyconcrete# pwd
/usr/local/lib/python3.9/site-packages/pyconcrete

root@cde87253aac7:/usr/local/lib/python3.9/site-packages/pyconcrete# ls
__init__.py  __pycache__  _pyconcrete.cpython-39-x86_64-linux-gnu.so  version.py

root@cde87253aac7:/usr/local/lib/python3.9/site-packages/pyconcrete# whereis pyconcrete
pyconcrete: /usr/local/bin/pyconcrete

root@cde87253aac7:/# python hack.py
Traceback (most recent call last):
  File "/hack.py", line 13, in <module>
    from script import *
ModuleNotFoundError: No module named 'script'

In my view, Django can only use partial encrypted solution. Is it this?

ZhaoQi99 avatar Dec 17 '24 15:12 ZhaoQi99

@Falldog Can you take a look at pyencrypt-pye when you have time? May be the project has the same problem as pyconcrete?

ZhaoQi99 avatar Dec 17 '24 15:12 ZhaoQi99

@ZhaoQi99 Yes, now Django can only use unsafe partial encrypted solution.

Unfortunately, pyencrypt-pye as well as any other software written in Python and launched by the "standard" Python interpreter is vulnerable from the start. That's why pyconcrete in full encrypted variant uses binary to launch pye files instead python

dx-77 avatar Dec 17 '24 18:12 dx-77

In my view, Django can only use partial encrypted solution. Is it this?

In develop & staging environment, you could encrypt django entrypoint manage.py and launch it by pyconcrete to achieve full encryption. But in production mode, the best practice should be launch django by uwsgi or gunicorn. If you want to fully encryption and you must make uwsgi or gunicorn able to import .pye and decrypt files.

Agree with @dx-77. pyencrypt-pye is more like partial encryption. Once the launcher is python default interpreter, and it's easy to hack by senior python engineer.

Falldog avatar Dec 18 '24 01:12 Falldog

After v1.1.0 released, the full encryption would be default option. Reference #119

In general case, you better should use the fully encryption mode (pyconcrete exe). For the web service case, such as Django or others, you could able consider partial encryption mode (pyconcrete lib). And you need to take care the environment you may expose to 3rd party about security.

Close the issue since fully encryption already be default option. And marshal.loads is not easy to be hacked.

Falldog avatar Aug 31 '25 02:08 Falldog