This is a very simple system based on sending JSON messages that seems to have a security vulnerability. There is a Python server (using JSON module included in standard library) which receives JSON objects and acts on them. If it gets {"req": "ping"}
, it simply returns {"resp": "pong"}
. It also has a command for setting volume, and one to change the admin password. The admin can send any JSON to this server. Here it is (server.py):
import json
import sys
def change_admin_password(p): pass # empty for test
def set_volume(v): pass # empty for test
def handle(js):
if (js["req"] == "ping"):
return {"resp": "pong"}
if (js["req"] == "change admin password"):
change_admin_password(js["args"]["password"])
return {"resp": "ok"}
if (js["req"] == "set volume"):
set_volume(int(js["args"]["volume"]))
return {"resp": "ok"}
print handle(json.load(sys.stdin))
It reads a command from stdin and processes it. Another script would read JSON objects from a network socket and pass them to this script.
Other users have to go through a proxy written in C++ using libjson. It simply blocks requests that should require admin privileges. For example, if a user tries to change the admin password, the proxy will reject the command:
$ echo '{"req": "change admin password", "args": {"password":"new"}}' | ./proxy
echo $?
1
Here is the code (proxy.cpp):
#include "libjson.h"
using namespace std;
int main() {
string json((istreambuf_iterator<char>(cin)), istreambuf_iterator<char>());
JSONNode n = libjson::parse(json);
string req = n.at("req").as_string();
if (req == "change admin password") {
return 1;
}
cout << n.write();
}
To use the proxy, the main script managing a socket would pipe data through the proxy and output of that to the Python server:
$ echo '{"req": "ping"}' | ./proxy | python server.py
{'resp': 'pong'}
$ echo '{"req": "set volume", "args": {"volume": 50}}' | ./proxy | python server.py
{'resp': 'ok'}
And if a restricted command is attempted by the user, it will fail as expected:
$ echo '{"req": "change admin password", "args": {"password": "new"}}' | ./proxy | python server.py
Traceback (most recent call last):
File "server.py", line 17, in <module>
[...]
But for some reason, if the "req" key is in the JSON twice (shouldn't it be illegal?), non admin users are able to change the admin password:
$ echo '{"req": "nothing", "req": "change admin password", "args": {"password": "new"}}' | ./proxy | python server.py
{'resp': 'ok'}
Why?
Trying Mike McMahon's workaround:
I tried using JSONNode.find
as a workaround, but it doesn't seem to be working.
I tried just iterating over all elements:
#include "libjson.h"
using namespace std;
int main() {
string json((istreambuf_iterator<char>(cin)), istreambuf_iterator<char>());
JSONNode n = libjson::parse(json);
for (JSONNode::json_iterator it = n.begin(); it != n.end(); it++) {
cout << "found one: " << it->at("x").as_string() << endl;
}
}
This works:
$ g++ proxy.cpp libjson.a -o proxy && ./proxy
In file included from libjson.h:4:0,
from proxy.cpp:1:
_internal/Source/JSONDefs.h:157:6: warning: #warning , Release build of libjson, but NDEBUG is not on [-Wcpp]
{"a": {"x": 1}, "b": {"x": 2}}
found one: 1
found one: 2
Except it segfaults if the JSON is invalid? Am I using the iterator wrong?
$ echo '{"a": {"x": 1}, {"b": {"x": 2}}' | ./proxy
found one: 1
Segmentation fault
I replaced the n.begin()
with n.find("y")
:
#include "libjson.h"
using namespace std;
int main() {
string json((istreambuf_iterator<char>(cin)), istreambuf_iterator<char>());
JSONNode n = libjson::parse(json);
for (JSONNode::json_iterator it = n.find("y"); it != n.end(); it++) {
cout << "found one: " << it->at("x").as_string() << endl;
}
}
It doesn't work at all. Am I using the iterator wrong?
g++ proxy.cpp libjson.a -o proxy && ./proxy
In file included from libjson.h:4:0,
from proxy.cpp:1:
_internal/Source/JSONDefs.h:157:6: warning: #warning , Release build of libjson, but NDEBUG is not on [-Wcpp]
{"y": {"x": 1}}
Segmentation fault
Another attempt at the workaround:
#include "libjson.h"
using namespace std;
int main() {
string json((istreambuf_iterator<char>(cin)), istreambuf_iterator<char>());
JSONNode n = libjson::parse(json);
for (JSONNode::json_iterator it = n.begin(); it != n.end(); it++) {
if (it->name() == "req" && it->as_string() == "change admin password")
{
cout << "found one " << endl;
}
}
}
It works!
$ echo '{"req": "change admin password"}' | ./proxy
found one
$ echo '{"req": "x", "req": "change admin password"}' | ./proxy
found one
$ echo '{"req": "change admin password", "req": "x"}' | ./proxy
found one
But still segfaults with invalid JSON input?
$ echo '{"req": "x", {"req": "x"}' | ./proxy
Segmentation fault