
When Java Web Start won’t listen: bending a JNLP app to your proxy
Pentesting isn’t always a fresh playground of modern frameworks and shiny APIs like programming… (just joking). Sometimes you’re staring at a decade-old Java Web Start client that won’t even run under the latest tools, and half your extensions refuse to install because they were built for Burp v1.2. In those cases, you cobble together legacy gadgets, bridge mismatched protocols, and rip apart binary payloads, all just to see what a stubborn app is hiding.
Last month I hit exactly that wall: a Java application delivered via JNLP. The catch? It lived behind a bastion accessed only through a PAM portal over VPN. And, most frustratingly, it simply ignored every proxy setting I threw at it: neither JVM flags nor OS-level variables seemed to work and, as if it wasn’t enough, it was Windows-based. Here’s how I combined multiple resources to force every socket into my proxy, decode Fast Infoset and Java-serialized SOAP, and expose serious flaws in client-side trust.
Peeking at the JNLP launcher
Here’s the essence of the offending master.jnlp
config file, with all real paths and names replaced by placeholders. You can see it demands Java 8, grants full permissions, and flags both Fast Infoset and Java serialization, not the most inviting startup file:
<jnlp spec="1.0+" codebase="https://{CLIENT_PLUGINS_PATH}/"> <information> <title>{APP_NAME}</title> <vendor>{VENDOR_NAME}</vendor> <description>{APP_NAME} Client</description> </information> <security> <all-permissions/> </security> <resources> <!-- Unsigned-jar workaround --> <property name="jnlp.fixPolicy" value="false"/> <!-- Core JRE requirement --> <j2se version="1.8.0+" href="http://java.sun.com/autodl/j2se" java-vm-args="-Xms1024M -Xmx1024M"/> <!-- Endpoints and features --> <property name="jnlp.server.baseurl" value="https://{BASE_URL}"/> <property name="jnlp.server.fastinfoset" value="true"/> <property name="jnlp.server.serialization" value="true"/> <property name="jnlp.mainclass" value="{MAIN_CLASS}"/> <!-- A few of the extension modules --> <extension name="com.example.plugin.alpha" href="app/plugin-alpha.jnlp"/> <extension name="com.example.plugin.beta" href="app/plugin-beta.jnlp"/> <extension name="com.example.plugin.gamma" href="app/plugin-gamma.jnlp"/> </resources> <application-desc main-class="{MAIN_CLASS}"> <argument>--branding</argument> </application-desc> </jnlp>
Normally you would run those apps with the javaws command where you can also specify the JVM args, but no matter how many -J-Dhttp.proxyHost=127.0.0.1
or -J-Dhttp.proxyPort=8080
flags I specified into the launch command, the JNLP runtime dropped them silently when it forked its own JVM.
Early, fruitless experiments
I began with testing every “official” proxy route. In Windows’ Settings → Network & Internet → Proxy I set a manual HTTP proxy to 127.0.0.1:8080 and loaded the corporate PAC URL, but zero traffic appeared. Turns out Internet Options’ LAN Settings were locked by group policy. The Java Control Panel’s Network Settings only let me choose “Direct connection” while all other modes stayed greyed out.
Furthermore, tweaking %APPDATA%\Oracle\Java\Deployment\deployment.properties had no effect, and setting HTTP_PROXY/HTTPS_PROXY/ALL_PROXY environment variables system-wide was similarly ignored. By the time javaws launched, it refused every high-level hint I fed it.
Forcing every socket through Burp
When high-level tricks failed, I knew I needed a more low-level approach, which led me to discover a Windows-based version of proxychains, hence I was lucky enough not to have to rewrite it from scratch.
The only issue was that…it only supports SOCKS5 proxies, while Burp clearly speaks HTTP.
SOCKS5 vs. HTTP Proxy
- SOCKS5: Layer-5 “socket” proxy that transparently tunnels TCP (and optionally UDP) streams. Protocol-agnostic.
- HTTP: Layer-7 proxy for HTTP/1.x, understanding headers like CONNECT for TLS.
Luckily, a SOCKS5 to HTTP(S) bridge exists for Windows, and it’s called http-proxy-to-socks (it also comes with binary releases!). In any case, such a bridge would have been trivial to implement, you’d just need to establish an initial HTTP connection with the client by correctly parsing the CONNECT line and then forwarding the remaining requests to the SOCKS5 listener.
The approach I followed was the following one:
- Launch Burp on Java 8
To keep compatibility with legacy plugins (spoiler for later), I ran Burp Suite v1.6.28 (from 2015) using the same JRE as the target; for some reason at the time only Burp Suite Pro was regularly updated, while Community Edition had a lot less releases.
"C:\Program Files (x86)\Java\jre1.8.0_112\bin\java.exe" -classpath burpsuite_free_v1.6.28.jar burp.StartBurp
Burp listened on 127.0.0.1:8080 for HTTP(S).
- Run socks-over-https
I used socks-over-https to turn Burp’s HTTP proxy into a SOCKS5 endpoint. My config.json told it to listen for SOCKS5 on 127.0.0.1:1080, issue HTTP CONNECT calls to Burp at 127.0.0.1:8080, and log everything for debugging:{ "log": { "dir": "C:\\Users\\PT\\Desktop\\socks-via-burp", "level": "debug" }, "settings": { "readBufferSize": 4096, "writeBufferSize": 4096 }, "proxies": [ { "socks": { "address": "127.0.0.1", "port": 1080 }, "http": { "address": "127.0.0.1", "port": 8080 } } ] }
Launched with the command:
socks-over-https -c config.json
- Wrap the JNLP with proxychains
Finally, I forced javaws to use that SOCKS5 endpoint by running under proxychains-windows:
.\proxychains_win32_x64.exe -f .\proxychains.conf
"C:\Program Files (x86)\Java\jre1.8.0_112\bin\javaws"
https://{CLIENT_PLUGINS_PATH}/master.jnlp
With a minimal proxychains.conf pointing at 127.0.0.1:1080, every socket, regardless of the launcher’s own settings, flowed through Burp’s HTTP proxy.
How proxychains-windows works under the hood
Rather than rely on JVM or OS proxy settings, proxychains-windows achieves redirection by injecting a small DLL that hooks into the Windows Sockets (Winsock) API. From there:
- When the application calls functions like socket() or connect(), proxychains intercepts these calls and transparently reroutes them.
- Every outbound TCP connection is piped to the local SOCKS5 server at 127.0.0.1:1080. The Java client remains oblivious, believing it’s talking directly to the remote server.
- You can stack multiple proxies, SOCKS5, then HTTP, then another SOCKS as needed. In our case, a single SOCKS5 hop sufficed before handing off to socks-over-https, but proxychains can handle far more complex chains.
- Since it operates below the JVM, proxychains is invisible to Java’s own proxy flags. And because it hooks at the system call level, group policy–enforced Windows proxy settings are irrelevant.
Peeling back Fast Infoset & Java serialization
With traffic finally visible in Burp, the bodies were opaque binaries. SOAP actions like querySerializedRequest hinted at two layers: Fast Infoset (binary XML) and Java object serialization. Here is an example of a Fast Infoset-compressed request, notice the specification both in the request and in the response.
Inspecting the WSEndpoint Interface
Decompiling the service interface confirmed which calls and types were in play. Here’s a heavily-templated excerpt:
@WebService(name="WSEndpoint", targetNamespace="{SERVICE_NAMESPACE}") public interface WSEndpoint { @WebMethod @Action(input="{SERVICE_NAMESPACE}/WSEndpoint/querySerializedRequest", output="{SERVICE_NAMESPACE}/WSEndpoint/querySerializedResponse") SerializedObject querySerialized( @WebParam(name="sessionToken") String sessionToken, @WebParam(name="queryName") String queryName, @WebParam(name="columns") List<String> columns, @WebParam(name="filter") QueryFilter filter ) throws ServiceException; // …plus methods like query2(), queryAll(), describeQuery(), etc. }
Those annotations and wrapper classes (e.g. QuerySerialized, QueryAllResponse) mapped exactly which SOAP actions to target and which payloads were Fast Infoset versus raw Java-serialized blobs.
Converting to Readable XML & Objects
I installed PortSwigger’s FISTA extension to translate application/fastinfoset frames into plain SOAP. For raw java.io.Serializable blobs, JDSer-ngng deserialized ObjectInputStream data into a browsable tree. There are many versions out there, just pick the one that works! At this point every SOAP request and data structure was revealed, no server or client modification needed.
While Fast Infoset is just lossless compression (binary XML), JDSer-ngng requires the app’s class definitions to correctly parse the objects. I thus ran the open-source jnlpdownloader tool against the master JNLP, gathered all JARs into a folder, and dropped them into Burp’s libs/. A quick reload bound every class name to its type and a new “Deserialized Java” tab appeared in Burp.
Client-side controls don’t offer much control
With the proxy chain and decoding pipeline locked in, I tampered freely with every SOAP endpoint. Because the server deferred virtually all access control to the client, I managed to achieve the following:
- Broken Access Control and IDOR (Insecure Direct Object Reference): fetching another user’s blob via queryUserInfo then feeding it into userConfig let me reset passwords at will.
- UI logic bypass: Java Swing-only checks turned ineffective once I accessed functionalities restricted to other user roles, no server-side ACL ever validated it.
- Full app takeover: auditing, admin routines, file exports… everything became accessible just by intercepting HTTP traffic.
This behavior tends to recur in mobile pentesting engagements where Certificate Pinning is one of the main initial hurdles. This layer is often considered sufficient to neglect more in-depth access control procedures, especially if the back-end isn’t shared with a desktop application.
Next time you’re up against a thick client that acts “proxy-unaware”, remember to combine the right vintage gadgets, force its sockets into your proxy, decode its binary whispers, and watch the vulnerabilities arise from this “unproxyable” app.
Discover how we can help you
Together, we’ll find the best solutions to tackle the challenges your business faces every day.