Messing with LD_LIBRARY_PATH for fun and profit
By Ricardo
Let me give you a context first.
I have a NAS. It’s a simple one, a 4-bay Asustor AS3104T. It works great and currently does Plex hardware encoding, store my backups and runs a few extra Docker containers for stuff I need. It’s great! It has also some nice stats on it, like usage and temperatures.
On the other side, I run Home Assistant, where I’ve been putting some information about the temperatures of my home servers and its drives. You know, stuff normal people want to automate, right? Like turning on fans, getting alerts if something is weird, etc. Normal people do this, right? Right?
Well, I wanted to get the NAS information on the HA. Simple right? Yeah.. not really. You see, the NAS does not have a simple way of obtaining this data. My standard approach is to check the /sys/devices/virtual/thermal tree, as it usually contains all the data I need
# ls -al /sys/devices/virtual/thermal/
total 0
drwxr-xr-x 7 root root 0 Nov 7 17:16 ./
drwxr-xr-x 19 root root 0 Oct 26 00:00 ../
drwxr-xr-x 3 root root 0 Nov 7 17:16 cooling_device0/
drwxr-xr-x 3 root root 0 Nov 7 17:16 cooling_device1/
drwxr-xr-x 3 root root 0 Nov 7 17:16 cooling_device2/
drwxr-xr-x 3 root root 0 Nov 7 17:16 cooling_device3/
drwxr-xr-x 3 root root 0 Nov 7 17:16 thermal_zone0/
# cat /sys/devices/virtual/thermal/thermal_zone0/temp
26800
# cat /sys/devices/virtual/thermal/cooling_device0/cur_state
0
# cat /sys/devices/virtual/thermal/cooling_device1/cur_state
0
# cat /sys/devices/virtual/thermal/cooling_device2/cur_state
0
# cat /sys/devices/virtual/thermal/cooling_device3/cur_state
0
Yeah, nothing. But the web interface must get it from somewhere, right? Turns it it does - from an API endpoint:
So, what is this sysinfo.cgi? A quick disk search revealed it it’s an actual binary on the system:
# file /volume0/usr/builtin/webman/portal/apis/information/sysinfo.cgi
/volume0/usr/builtin/webman/portal/apis/information/sysinfo.cgi: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=4b17db272e002be02408d9523327810e00a54b47, stripped
So, it’s a binary. But how does it work? I mean, how to I call it? Can I call it directly?
# /volume0/usr/builtin/webman/portal/apis/information/sysinfo.cgi
Content-type: text/plain; charset=utf-8
{ "success": false, "error_code": 5000 }
Ok, fair enough, calling it straight away doesn’t help. I need to understand how the HTTP daemon is calling it, so I decided to replace it with a shell script that would later call the original one. The volume where this file is stored is writeable, so this was pretty easy.
# cd /volume0/usr/builtin/webman/portal/apis/information
# mv sysinfo.cgi sysinfo.cgi.old
# cat sysinfo.cgi
#!/bin/sh
export > /tmp/sysinfo_dump.log
echo "--" >> /tmp/sysinfo_dump.log
echo "$@" >> /tmp/sysinfo_dump.log
/volume0/usr/builtin/webman/portal/apis/information/sysinfo.cgi.old $@
The dump file gave me all the information I needed, basically:
export CONTENT_LENGTH='0'
export DOCUMENT_ROOT='/usr/webman/'
export GATEWAY_INTERFACE='CGI/1.1'
export HTTP_ACCEPT='*/*'
export HTTP_ACCEPT_ENCODING='gzip, deflate'
export HTTP_ACCEPT_LANGUAGE='en,de;q=0.9,de-DE;q=0.8,en-US;q=0.7'
export HTTP_CACHE_CONTROL='no-cache'
export HTTP_CONNECTION='keep-alive'
export HTTP_CONTENT_LENGTH='0'
export HTTP_COOKIE='(...)'
export HTTP_DNT='1'
export HTTP_HOST='xxx.lan:8000'
export HTTP_ORIGIN='http://xxx.lan:8000'
export HTTP_PRAGMA='no-cache'
export HTTP_REFERER='http://xxx.lan:8000/portal/'
export HTTP_USER_AGENT='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36'
export HTTP_X_REQUESTED_WITH='XMLHttpRequest'
export PWD='/volume0/usr/builtin/webman/portal/apis/information'
export QUERY_STRING='sid=YgqnX.nhdkhyQoty&act=sys&_dc=1604785963061'
export REDIRECT_STATUS='200'
export REMOTE_ADDR='172.16.x.y'
export REMOTE_PORT='55402'
export REQUEST_METHOD='POST'
export REQUEST_URI='/portal/apis/information/sysinfo.cgi?sid=YgqnX.nhdkhyQoty&act=sys&_dc=1604785963061'
export SCRIPT_FILENAME='/usr/webman/portal/apis/information/sysinfo.cgi'
export SCRIPT_NAME='/portal/apis/information/sysinfo.cgi'
export SERVER_ADDR='172.16.x.x'
export SERVER_NAME='xxx.lan'
export SERVER_PORT='8000'
export SERVER_PROTOCOL='HTTP/1.1'
export SERVER_SOFTWARE='lighttpd/1.4.29-devel-187'
So lighttpd is basically writing everything as environment variables and calling the binary. A bunch of trial and error later, I managed to figure out it only needs the QUERY_STRING
variable - everything else can be removed. The query string is made of 3 arguments:
- sid: the token - we receive that after logging in
- act: not sure, but I guess it’s what are we trying to get the info from
- _dc: current timestamp, probably for to avoid caching the response on lighttpd
The first thing I tested is if the token is checked - and it is. It took me a while to figure out against what, as it would need some kind of table to keep them. Logging out of the web interface invalidates the token, so it must be stored somewhere. Turns out it’s in the disk, at /var/run/login.log
(weird name, I know):
# cat /var/run/login.log
YgqnX.nhdkhyQoty 00129B47 00128D06 -------- 000003E7 172.16.x.y admin
This is a tab-spaced file containg the token, two numbers, dashes, another number, the source IP for this session and the username. The dashes will include a “D” if you logged out - probably meaning disabled. In theory, this would be enough to make the call, as we could get this using grep
and cut
:
# TOKEN=$(cat /var/run/login.log | grep -Pv '\tD' | cut -d$'\t' -f1)
# QUERY_STRING="sid=$TOKEN&act=sys&_dc=1604780579403" ./sysinfo.cgi
Content-type: text/plain; charset=utf-8
{ "success": true,"model":"AS3104T", "cpu":"Intel® Celeron™ CPU @ 1.60GHz", "mem":"2048", "serial":"XXX", "os":"3.5.1.R8C1", "timezone":"(GMT-03:00) Brasilia", "time":"1604786559", "year":"2020", "month":"11", "day":"7", "hour":"20", "minute":"2", "DateFormat":"1", "TimeFormat":"1", "uptime":"1219624", "cputemp":"56", "bios":"2.23", "bootloader":"", "fan_speed": ["528"], "systemp":"37","aid":""}
This, however, wouldn’t work if there was no active token. I know for a fact that the web interface will log me out after a few days (probably what those numbers mean), so this wouldn’t work for an automated process. This would require a logged user, which I’m not willing to have.
So, I needed a different approach. How about checking the binary, what it does and so on? How about running a simple strace
on it?
Tracing the execution showed a bunch of interesting things. The first thing that caught my eye was a read against a hardware device:
# grep temp sysinfo.log | grep -Ev "ENOENT|model"
access("/sys/devices/platform/coretemp.0/hwmon/hwmon1/temp2_input", R_OK) = 0
# cat /sys/devices/platform/coretemp.0/hwmon/hwmon1/temp2_input
56000
So this is how it reads the CPU temperature! Nice! But unfortunately there are no other devices around that tree and I didn’t manage to get any useful information from any other (virtual?) device around there.
Ok, let’s take a different approach. How about cracking the session? The NAS is x86-x64, so we could modify the binary to not check the token and just return the information we want. Or maybe we could just modify which file it reads to something we can control: this would be way easier and faster. The tracing did indeed show it was reading the login.log
file:
stat("/var/run/login.log", {st_mode=S_IFREG|0600, st_size=288, ...}) = 0
open("/var/run/login.log", O_RDONLY) = 4
read(4, "m.KmXyIBGkgkYZYp\t00127814\t001275"..., 288) = 288
close(4)
Running strings
on the sysinfo.cgi
binary didn’t yield anything login related though, so it must get this information from somewhere. Maybe some library?
# grep "open(" sysinfo.log | grep "lib/" | grep -v ENOENT
open("/usr/builtin/lib/libbackup.so", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/libnasman.so", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/libservice.so", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/libcgi.so", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/libgeneral.so", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/libndal.so", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/libnhal.so", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/libhalgeneral.so", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/libjson-c.so.2", O_RDONLY|O_CLOEXEC) = 3
(...)
Ok, so that shows some very promising libraries! libnasman
and libcgi
seem like some custom ones, so let’s check them!
# strings /usr/lib/libnasman.so | grep login.log
/var/run/login.log
/usr/builtin/etc/login/login_template.conf
# strings /usr/lib/libcgi.so | grep login.log
/var/run/login.log
Nice. Now we just have to change them without breaking the whole OS and being update-proof. Why take the easy path when we can make it harder, right?
So… let’s replace it. Let’s change it to something we control. How about /tmp/fake.login
?
Null-terminated strings can have as many 0x00
at the end. As long as we don’t overflow that string, everything should be just fine.
Doing that to libnasman
had no effect, still failing the execution, but replacing on libcgi
did the trick:
# QUERY_STRING="sid=foobar&act=sys&_dc=999" LD_LIBRARY_PATH=. strace /volume0/usr/builtin/webman/portal/apis/information/./sysinfo.cgi 2>&1 | grep fake.login
open("/var/lock/fake.login.lck", O_RDWR|O_CREAT|O_NOATIME, 0666) = 3
stat("/tmp/fake.login", {st_mode=S_IFREG|0600, st_size=72, ...}) = 0
open("/tmp/fake.login", O_RDONLY) = 4
open("/tmp/fake.login.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0600) = 4
rename("/tmp/fake.login.tmp", "/tmp/fake.login") = 0
You see, all I had to do was to copy the library to the current folder, modify it and set the LD_LIBRARY_PATH
, which will tell the OS where it should look for dynamic libraries. Since libcgi
is loaded dynamically, this allows us to have a modified copy of it to override the default behaviour. By doing so, we stick with the original binary, but everything elese that needs this libcgi
for checking the session will be compromised if I a custom library path for it. Cool!
So all that’s left is to write the fake.login
file. We didn’t modify the binary, so it still needs to check for a session. Nothing that a simple script won’t fix:
# cat fakecgi.sh
export QUERY_STRING='sid=-fake-cgi-token-&act=sys&_dc=1604780579403'
export LD_LIBRARY_PATH=.
echo "-fake-cgi-token- 001294C1 00128D06 -------- 000003E7 172.16.x.y admin" > /tmp/fake.login
/volume0/usr/builtin/webman/portal/apis/information/./sysinfo.cgi | grep "{"
# ./fakecgi.sh
{ "success": true,"model":"AS3104T", "cpu":"Intel® Celeron™ CPU @ 1.60GHz", "mem":"2048", "serial":"XXX", "os":"3.5.1.R8C1", "timezone":"(GMT-03:00) Brasilia", "time":"1604787981", "year":"2020", "month":"11", "day":"7", "hour":"20", "minute":"26", "DateFormat":"1", "TimeFormat":"1", "uptime":"1221046", "cputemp":"56", "bios":"2.23", "bootloader":"", "fan_speed": ["529"], "systemp":"37","aid":""}
That’s it. Now all I have to do is to use my trusty jq
to parse that JSON and get whatever I want/need from it. In my case I wanted to know the CPU and system temperatures, plus the fan speed:
# ./fakecgi.sh | jq -r ".cputemp"
57
# ./fakecgi.sh | jq -r ".systemp"
37
# ./fakecgi.sh | jq -r ".fan_speed[0]"
527
Update: or so I thought.
A few minutes I published this post, I started having some errors. Turns out those binaries also check the expiration date on the sessions. And again by trial-and-error I managed to figure out that the second value (the first number) is the expiration date. Refreshing the web page would change that value in the real file, increasing it by the same number of seconds I waited before pressing F5. The second number (third field) seems to be the session start date, as it is smaller than the previous. And this information is shown
Replacing 001294C1
with 0FFFFFFF
did the trick and the script started working again. Just for the sake of it, I’ve also replace the 00128D06
with 00000000
. Honestly, the 00128D06
is a mystery for me still - what is based on? Seconds after something? But what? Subtracting 00128D06
from 001294C1
gave about 7753, which, once added to the original login time, shows the exact time I did the last API call. Interesting.
For now, since the script is working again, I won’t bother figuring it out. Let’s see for how long it works! Ah, and this is the final result: